Budget Model Enhancements: - Add reserved_amount property (sum of pending requests) - Add available_amount property (remaining - reserved) - Add _quarter_months() helper method API Endpoints: - Add /budgets/my-department/ endpoint for user's department budget - Add /stats/overview/ for procurement statistics - Add /stats/by-department/ for department-level statistics - Register ProcurementStatsViewSet in urls.py Budget Alerts: - Add _check_budget_alert() method in ProcurementRequestViewSet - Send notifications when budget < 20% (low) or < 10% (critical) - Alerts sent to department head via NotificationService Serializers: - Add reserved_amount, available_amount to BudgetSerializer - Add BudgetDetailSerializer with pending_requests list Permissions: - Fix led_departments → headed_departments in BudgetViewSet - Allow my_department endpoint for all authenticated users Tests: - Add test_budget.py with 11 new tests - TestBudgetReservedAmount (3 tests) - TestBudgetMyDepartmentEndpoint (3 tests) - TestProcurementStatsEndpoints (4 tests) - TestBudgetAlerts (1 test) All 52 procurement tests passing: ✅ 20/20 Model tests ✅ 12/12 API tests ✅ 9/9 Workflow tests ✅ 11/11 Budget tests (NEW)
301 lines
11 KiB
Python
301 lines
11 KiB
Python
"""
|
|
Тесты для Budget - reserved_amount, my_department, stats endpoints.
|
|
"""
|
|
from decimal import Decimal
|
|
from django.utils import timezone
|
|
from django.contrib.auth.models import Permission
|
|
|
|
import pytest
|
|
from rest_framework import status
|
|
|
|
from procurement.models import (
|
|
Budget,
|
|
ProcurementRequest,
|
|
)
|
|
from procurement.constants import ProcurementStatus, UrgencyLevel
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestBudgetReservedAmount:
|
|
"""Тесты для reserved_amount и available_amount."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self, department_factory, user_factory, link_factory):
|
|
"""Настройка тестовых данных."""
|
|
self.department = department_factory(name='IT Reserved Test')
|
|
now = timezone.now()
|
|
self.quarter = (now.month - 1) // 3 + 1
|
|
|
|
self.budget = Budget.objects.create(
|
|
department=self.department,
|
|
year=now.year,
|
|
quarter=self.quarter,
|
|
allocated_amount=Decimal('100000.00'),
|
|
spent_amount=Decimal('20000.00'),
|
|
)
|
|
|
|
self.user = user_factory(
|
|
email='budgetuser@test.com',
|
|
first_name='Test',
|
|
last_name='User',
|
|
)
|
|
link_factory(self.user, self.department, is_active=True)
|
|
|
|
def test_reserved_amount_no_pending(self):
|
|
"""Тест: reserved_amount = 0 если нет pending заявок."""
|
|
assert self.budget.reserved_amount == Decimal('0.00')
|
|
|
|
def test_reserved_amount_with_pending(self):
|
|
"""Тест: reserved_amount считает сумму pending заявок."""
|
|
# Создаём pending заявку
|
|
ProcurementRequest.objects.create(
|
|
title='Заявка 1',
|
|
description='Описание',
|
|
department=self.department,
|
|
requestor=self.user,
|
|
status=ProcurementStatus.PENDING,
|
|
urgency=UrgencyLevel.LOW,
|
|
estimated_cost=Decimal('5000.00'),
|
|
)
|
|
ProcurementRequest.objects.create(
|
|
title='Заявка 2',
|
|
description='Описание',
|
|
department=self.department,
|
|
requestor=self.user,
|
|
status=ProcurementStatus.PENDING,
|
|
urgency=UrgencyLevel.MEDIUM,
|
|
estimated_cost=Decimal('3000.00'),
|
|
)
|
|
|
|
# reserved должен быть 8000
|
|
assert self.budget.reserved_amount == Decimal('8000.00')
|
|
|
|
def test_available_amount(self):
|
|
"""Тест: available_amount = remaining - reserved."""
|
|
ProcurementRequest.objects.create(
|
|
title='Pending заявка',
|
|
description='Описание',
|
|
department=self.department,
|
|
requestor=self.user,
|
|
status=ProcurementStatus.PENDING,
|
|
urgency=UrgencyLevel.LOW,
|
|
estimated_cost=Decimal('10000.00'),
|
|
)
|
|
|
|
# remaining = 100000 - 20000 = 80000
|
|
# reserved = 10000
|
|
# available = 80000 - 10000 = 70000
|
|
assert self.budget.remaining_amount == Decimal('80000.00')
|
|
assert self.budget.reserved_amount == Decimal('10000.00')
|
|
assert self.budget.available_amount == Decimal('70000.00')
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestBudgetMyDepartmentEndpoint:
|
|
"""Тесты для /budgets/my-department/ endpoint."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(
|
|
self, api_client, department_factory, user_factory, link_factory
|
|
):
|
|
"""Настройка тестовых данных."""
|
|
self.client = api_client
|
|
|
|
self.department = department_factory(name='IT My Dept Test')
|
|
|
|
now = timezone.now()
|
|
self.quarter = (now.month - 1) // 3 + 1
|
|
|
|
self.budget = Budget.objects.create(
|
|
department=self.department,
|
|
year=now.year,
|
|
quarter=self.quarter,
|
|
allocated_amount=Decimal('100000.00'),
|
|
spent_amount=Decimal('30000.00'),
|
|
)
|
|
|
|
self.user = user_factory(
|
|
email='mydeptuser@test.com',
|
|
first_name='Test',
|
|
last_name='User',
|
|
)
|
|
link_factory(self.user, self.department, is_active=True)
|
|
self.user_factory = user_factory
|
|
|
|
def test_my_department_success(self):
|
|
"""Тест: получение бюджета своего отдела."""
|
|
self.client.force_authenticate(user=self.user)
|
|
|
|
response = self.client.get('/api/procurement/budgets/my-department/')
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert response.data['department'] == self.department.id
|
|
allocated = Decimal(response.data['allocated_amount'])
|
|
assert allocated == Decimal('100000.00')
|
|
spent = Decimal(response.data['spent_amount'])
|
|
assert spent == Decimal('30000.00')
|
|
remaining = Decimal(response.data['remaining_amount'])
|
|
assert remaining == Decimal('70000.00')
|
|
assert 'reserved_amount' in response.data
|
|
assert 'available_amount' in response.data
|
|
|
|
def test_my_department_no_department(self):
|
|
"""Тест: 404 если пользователь не в отделе."""
|
|
user_no_dept = self.user_factory(
|
|
email='nodept@test.com',
|
|
first_name='No',
|
|
last_name='Dept',
|
|
)
|
|
self.client.force_authenticate(user=user_no_dept)
|
|
|
|
response = self.client.get('/api/procurement/budgets/my-department/')
|
|
|
|
assert response.status_code == status.HTTP_404_NOT_FOUND
|
|
|
|
def test_my_department_no_budget(self):
|
|
"""Тест: 404 если нет бюджета на текущий квартал."""
|
|
# Удаляем бюджет
|
|
self.budget.delete()
|
|
|
|
self.client.force_authenticate(user=self.user)
|
|
|
|
response = self.client.get('/api/procurement/budgets/my-department/')
|
|
|
|
assert response.status_code == status.HTTP_404_NOT_FOUND
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestProcurementStatsEndpoints:
|
|
"""Тесты для /stats/ endpoints."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(
|
|
self, api_client, department_factory, user_factory, link_factory
|
|
):
|
|
"""Настройка тестовых данных."""
|
|
self.client = api_client
|
|
|
|
self.department = department_factory(name='IT Stats Test')
|
|
|
|
self.user = user_factory(
|
|
email='statsuser@test.com',
|
|
first_name='Test',
|
|
last_name='User',
|
|
)
|
|
link_factory(self.user, self.department, is_active=True)
|
|
|
|
self.staff_user = user_factory(
|
|
email='statsstaff@test.com',
|
|
first_name='Staff',
|
|
last_name='User',
|
|
)
|
|
self.staff_user.is_staff = True
|
|
self.staff_user.save()
|
|
|
|
# Создаём несколько заявок
|
|
for i in range(3):
|
|
ProcurementRequest.objects.create(
|
|
title=f'Заявка {i+1}',
|
|
description='Описание',
|
|
department=self.department,
|
|
requestor=self.user,
|
|
status=ProcurementStatus.PENDING,
|
|
urgency=UrgencyLevel.MEDIUM,
|
|
estimated_cost=Decimal('5000.00'),
|
|
)
|
|
|
|
def test_stats_overview(self):
|
|
"""Тест: получение общей статистики."""
|
|
self.client.force_authenticate(user=self.user)
|
|
|
|
response = self.client.get('/api/procurement/stats/overview/')
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert 'total_requests' in response.data
|
|
assert 'pending_requests' in response.data
|
|
assert 'by_status' in response.data
|
|
assert 'by_urgency' in response.data
|
|
|
|
def test_stats_by_department_staff(self):
|
|
"""Тест: статистика по отделам для staff."""
|
|
self.client.force_authenticate(user=self.staff_user)
|
|
|
|
response = self.client.get('/api/procurement/stats/by-department/')
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert isinstance(response.data, list)
|
|
|
|
def test_stats_by_department_regular_user(self):
|
|
"""Тест: 403 для обычного пользователя."""
|
|
self.client.force_authenticate(user=self.user)
|
|
|
|
response = self.client.get('/api/procurement/stats/by-department/')
|
|
|
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
|
|
|
def test_stats_by_department_head(self):
|
|
"""Тест: руководитель отдела видит статистику своих отделов."""
|
|
# Делаем пользователя руководителем
|
|
self.department.head = self.user
|
|
self.department.save()
|
|
|
|
self.client.force_authenticate(user=self.user)
|
|
|
|
response = self.client.get('/api/procurement/stats/by-department/')
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert len(response.data) == 1
|
|
assert response.data[0]['department']['id'] == self.department.id
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestBudgetAlerts:
|
|
"""Тесты для алертов бюджета."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(
|
|
self, api_client, department_factory, user_factory, link_factory
|
|
):
|
|
"""Настройка тестовых данных."""
|
|
self.client = api_client
|
|
|
|
self.department = department_factory(name='IT Alerts Test')
|
|
|
|
now = timezone.now()
|
|
self.quarter = (now.month - 1) // 3 + 1
|
|
|
|
# Бюджет почти исчерпан (95% использовано)
|
|
self.budget = Budget.objects.create(
|
|
department=self.department,
|
|
year=now.year,
|
|
quarter=self.quarter,
|
|
allocated_amount=Decimal('100000.00'),
|
|
spent_amount=Decimal('95000.00'), # 95% использовано
|
|
)
|
|
|
|
self.requestor = user_factory(
|
|
email='alertsrequestor@test.com',
|
|
first_name='Requestor',
|
|
last_name='User',
|
|
)
|
|
link_factory(self.requestor, self.department, is_active=True)
|
|
|
|
# Руководитель отдела
|
|
self.dept_head = user_factory(
|
|
email='alertshead@test.com',
|
|
first_name='Department',
|
|
last_name='Head',
|
|
)
|
|
link_factory(self.dept_head, self.department, is_active=True)
|
|
self.department.head = self.dept_head
|
|
self.department.save()
|
|
|
|
# Даём права на approve
|
|
perm = Permission.objects.get(codename='approve_procurementrequest')
|
|
self.dept_head.user_permissions.add(perm)
|
|
|
|
def test_budget_utilization_above_90(self):
|
|
"""Тест: бюджет используется более чем на 90%."""
|
|
assert self.budget.utilization_percentage == Decimal('95.00')
|
|
assert self.budget.remaining_amount == Decimal('5000.00')
|