feat: add procurement integration with notifications and real-time
Stage 7 - Integration: - Create notification types migration for procurement - Fix signals to use pre_save for status tracking - Add broadcast_request_update function for WebSocket events - Add procurement_update handler in UserConsumer - Add 6 integration tests for signals and notifications - Update README with integration details All 72 tests passing (66 base + 6 integration).
This commit is contained in:
parent
53a8f0e6b4
commit
d04b5e38c2
@ -0,0 +1,119 @@
|
||||
"""
|
||||
Миграция данных: создание типов уведомлений для модуля закупок.
|
||||
"""
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def create_procurement_notification_types(apps, schema_editor):
|
||||
"""Создать категорию и типы уведомлений для закупок."""
|
||||
NotificationCategory = apps.get_model('notifications', 'NotificationCategory')
|
||||
NotificationType = apps.get_model('notifications', 'NotificationType')
|
||||
|
||||
# Создаём категорию
|
||||
category, _ = NotificationCategory.objects.get_or_create(
|
||||
code='procurement',
|
||||
defaults={
|
||||
'name': 'Закупки',
|
||||
'description': 'Уведомления модуля закупок и инвентаризации',
|
||||
'icon': 'bi-cart3',
|
||||
'color': '#17a2b8',
|
||||
'order': 60,
|
||||
}
|
||||
)
|
||||
|
||||
# Типы уведомлений
|
||||
notification_types = [
|
||||
{
|
||||
'code': 'procurement_new_request',
|
||||
'name': 'Новая заявка на закупку',
|
||||
'description': 'Уведомление о создании новой заявки на закупку',
|
||||
'priority': 'normal',
|
||||
'default_channels': {'web': True, 'email': True, 'telegram': False},
|
||||
},
|
||||
{
|
||||
'code': 'procurement_pending_approval',
|
||||
'name': 'Требуется согласование',
|
||||
'description': 'Заявка ожидает вашего согласования',
|
||||
'priority': 'high',
|
||||
'default_channels': {'web': True, 'email': True, 'telegram': True},
|
||||
},
|
||||
{
|
||||
'code': 'procurement_approved',
|
||||
'name': 'Заявка одобрена',
|
||||
'description': 'Ваша заявка на закупку была одобрена',
|
||||
'priority': 'normal',
|
||||
'default_channels': {'web': True, 'email': True, 'telegram': False},
|
||||
},
|
||||
{
|
||||
'code': 'procurement_rejected',
|
||||
'name': 'Заявка отклонена',
|
||||
'description': 'Ваша заявка на закупку была отклонена',
|
||||
'priority': 'high',
|
||||
'default_channels': {'web': True, 'email': True, 'telegram': True},
|
||||
},
|
||||
{
|
||||
'code': 'procurement_stage_approved',
|
||||
'name': 'Этап согласования пройден',
|
||||
'description': 'Один из этапов согласования заявки завершён',
|
||||
'priority': 'low',
|
||||
'default_channels': {'web': True, 'email': False, 'telegram': False},
|
||||
},
|
||||
{
|
||||
'code': 'procurement_completed',
|
||||
'name': 'Закупка завершена',
|
||||
'description': 'Закупка по заявке успешно завершена',
|
||||
'priority': 'normal',
|
||||
'default_channels': {'web': True, 'email': True, 'telegram': False},
|
||||
},
|
||||
{
|
||||
'code': 'equipment_transferred',
|
||||
'name': 'Оборудование передано',
|
||||
'description': 'Оборудование передано в ваш отдел или вам',
|
||||
'priority': 'normal',
|
||||
'default_channels': {'web': True, 'email': False, 'telegram': False},
|
||||
},
|
||||
{
|
||||
'code': 'equipment_maintenance',
|
||||
'name': 'Обслуживание оборудования',
|
||||
'description': 'Запланировано или завершено обслуживание оборудования',
|
||||
'priority': 'low',
|
||||
'default_channels': {'web': True, 'email': False, 'telegram': False},
|
||||
},
|
||||
]
|
||||
|
||||
for nt_data in notification_types:
|
||||
NotificationType.objects.get_or_create(
|
||||
code=nt_data['code'],
|
||||
defaults={
|
||||
'category': category,
|
||||
'name': nt_data['name'],
|
||||
'description': nt_data['description'],
|
||||
'priority': nt_data['priority'],
|
||||
'default_channels': nt_data['default_channels'],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def reverse_procurement_notification_types(apps, schema_editor):
|
||||
"""Удалить типы уведомлений для закупок."""
|
||||
NotificationCategory = apps.get_model('notifications', 'NotificationCategory')
|
||||
NotificationType = apps.get_model('notifications', 'NotificationType')
|
||||
|
||||
NotificationType.objects.filter(code__startswith='procurement_').delete()
|
||||
NotificationType.objects.filter(code__startswith='equipment_').delete()
|
||||
NotificationCategory.objects.filter(code='procurement').delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('notifications', '0004_alter_telegramuser_telegram_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
create_procurement_notification_types,
|
||||
reverse_procurement_notification_types,
|
||||
),
|
||||
]
|
||||
@ -2,35 +2,40 @@
|
||||
|
||||
## Описание
|
||||
|
||||
Модуль для управления внутренними закупками по отделам и учета оборудования/инвентаря.
|
||||
Модуль для управления внутренними закупками по отделам и учета оборудования/инвентаря с полной интеграцией уведомлений и real-time обновлений.
|
||||
|
||||
## Основные возможности
|
||||
|
||||
### 1. Заявки на закупку
|
||||
- Создание заявок с позициями (товары/услуги)
|
||||
- Автоматический маршрут согласования по сумме:
|
||||
- До 10,000₽ — только руководитель отдела
|
||||
- 10,000-50,000₽ — руководитель отдела + финансовый менеджер
|
||||
- Свыше 50,000₽ — руководитель отдела + фин. менеджер + директор
|
||||
- Отслеживание статуса: черновик → согласование → одобрено → в работе → завершено
|
||||
- Автоматический маршрут согласования по ролям:
|
||||
- Руководитель отдела
|
||||
- Финансовый менеджер
|
||||
- Генеральный директор
|
||||
- Отслеживание статуса: черновик → согласование → одобрено → заказано → получено → завершено
|
||||
- Уровни срочности: низкая, средняя, высокая, критическая
|
||||
- **Real-time уведомления** при изменении статуса через WebSocket
|
||||
- **Email уведомления** для согласующих
|
||||
|
||||
### 2. Бюджетный контроль
|
||||
- Бюджеты по отделам и кварталам
|
||||
- Автоматическая проверка доступности средств при создании заявки
|
||||
- Отслеживание использования бюджета в процентах
|
||||
- Резервирование средств на период согласования
|
||||
|
||||
### 3. Инвентаризация оборудования
|
||||
- Учет оборудования с инвентарными номерами
|
||||
- QR-коды для маркировки (автогенерация)
|
||||
- Иерархические категории оборудования
|
||||
- Статусы: доступно, в использовании, на обслуживании, в ремонте, списано, утеряно
|
||||
- Статусы: на складе, в использовании, на обслуживании, списано
|
||||
- Привязка к отделам и ответственным лицам
|
||||
- Отслеживание гарантийных сроков
|
||||
- История передач оборудования между отделами
|
||||
|
||||
### 4. Техническое обслуживание
|
||||
- История обслуживания оборудования
|
||||
- Типы ТО: осмотр, плановое ТО, ремонт, модернизация, чистка
|
||||
- Планирование следующего обслуживания
|
||||
- Типы ТО: осмотр, плановое ТО, ремонт, модернизация
|
||||
- Учёт затрат на обслуживание
|
||||
|
||||
### 5. Поставщики
|
||||
- База данных поставщиков
|
||||
@ -46,7 +51,31 @@
|
||||
5. **Equipment** — оборудование/инвентарь
|
||||
6. **EquipmentCategory** — категории оборудования (иерархические)
|
||||
7. **MaintenanceRecord** — записи техобслуживания
|
||||
8. **Supplier** — поставщики
|
||||
8. **EquipmentTransferLog** — история передач оборудования
|
||||
9. **Supplier** — поставщики
|
||||
|
||||
## Интеграция с системой
|
||||
|
||||
### Уведомления
|
||||
Модуль полностью интегрирован с системой уведомлений:
|
||||
- `procurement_new_request` — новая заявка создана
|
||||
- `procurement_pending_approval` — требуется согласование (высокий приоритет)
|
||||
- `procurement_approved` — заявка одобрена
|
||||
- `procurement_rejected` — заявка отклонена (высокий приоритет)
|
||||
- `procurement_stage_approved` — этап согласования пройден
|
||||
- `procurement_completed` — закупка завершена
|
||||
- `equipment_transferred` — оборудование передано
|
||||
- `equipment_maintenance` — обслуживание оборудования
|
||||
|
||||
### WebSocket события
|
||||
Real-time обновления через `realtime.consumers.UserConsumer`:
|
||||
- `procurement_update` — изменение статуса заявки
|
||||
- События транслируются всем пользователям отдела и создателю заявки
|
||||
|
||||
### Сигналы
|
||||
- `pre_save` — отслеживание изменений статуса
|
||||
- `post_save` — отправка уведомлений и broadcast событий
|
||||
- Автоматическое резервирование/освобождение бюджета
|
||||
|
||||
## Stage 1: Базовая структура ✅
|
||||
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
"""
|
||||
Сигналы для модуля закупок.
|
||||
Автоматическая отправка уведомлений при изменении статусов.
|
||||
WebSocket события для real-time обновлений.
|
||||
"""
|
||||
|
||||
from django.db.models.signals import post_save
|
||||
import logging
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
from django.db.models.signals import post_save, pre_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from notifications.services import NotificationService
|
||||
@ -11,12 +16,61 @@ from notifications.services import NotificationService
|
||||
from .constants import ApprovalStatus, ProcurementStatus
|
||||
from .models import Approval, ProcurementRequest
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def broadcast_request_update(request, event_type='request_updated'):
|
||||
"""Отправить WebSocket событие об обновлении заявки."""
|
||||
try:
|
||||
channel_layer = get_channel_layer()
|
||||
if not channel_layer:
|
||||
return
|
||||
|
||||
# Данные заявки для broadcast
|
||||
data = {
|
||||
'id': request.id,
|
||||
'title': request.title,
|
||||
'status': request.status,
|
||||
'status_display': request.get_status_display(),
|
||||
'department_id': request.department_id,
|
||||
'requestor_id': request.requestor_id,
|
||||
}
|
||||
|
||||
# Отправляем всем пользователям отдела
|
||||
group_name = f'procurement_dept_{request.department_id}'
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
group_name,
|
||||
{
|
||||
'type': 'procurement_update',
|
||||
'event': event_type,
|
||||
'data': data,
|
||||
}
|
||||
)
|
||||
|
||||
# Отправляем создателю заявки
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
f'user_{request.requestor_id}',
|
||||
{
|
||||
'type': 'procurement_update',
|
||||
'event': event_type,
|
||||
'data': data,
|
||||
}
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"[Procurement WS] Broadcast {event_type} for request {request.id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[Procurement WS] Broadcast failed: {e}")
|
||||
|
||||
|
||||
@receiver(post_save, sender=ProcurementRequest)
|
||||
def notify_on_request_status_change(sender, instance, created, **kwargs):
|
||||
"""Отправить уведомление при изменении статуса заявки."""
|
||||
if created:
|
||||
# Новая заявка создана
|
||||
# Новая заявка создана - broadcast
|
||||
broadcast_request_update(instance, 'request_created')
|
||||
|
||||
if instance.department.head:
|
||||
NotificationService.create_notification(
|
||||
recipient=instance.department.head,
|
||||
@ -39,6 +93,9 @@ def notify_on_request_status_change(sender, instance, created, **kwargs):
|
||||
if old_status == new_status:
|
||||
return
|
||||
|
||||
# Broadcast обновления статуса
|
||||
broadcast_request_update(instance, 'request_status_changed')
|
||||
|
||||
# Статус изменился
|
||||
if new_status == ProcurementStatus.PENDING:
|
||||
# Заявка отправлена на согласование
|
||||
@ -150,21 +207,23 @@ def notify_requestor(request, notification_type_code, title, message):
|
||||
|
||||
|
||||
# Сохраняем исходный статус перед сохранением
|
||||
@receiver(post_save, sender=ProcurementRequest)
|
||||
@receiver(pre_save, sender=ProcurementRequest)
|
||||
def save_original_status(sender, instance, **kwargs):
|
||||
"""Сохранить оригинальный статус для отслеживания изменений."""
|
||||
if instance.pk:
|
||||
original = ProcurementRequest.objects.filter(
|
||||
pk=instance.pk
|
||||
).first()
|
||||
if original:
|
||||
try:
|
||||
original = ProcurementRequest.objects.get(pk=instance.pk)
|
||||
instance._original_status = original.status
|
||||
except ProcurementRequest.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
@receiver(post_save, sender=Approval)
|
||||
@receiver(pre_save, sender=Approval)
|
||||
def save_original_approval_status(sender, instance, **kwargs):
|
||||
"""Сохранить оригинальный статус согласования."""
|
||||
if instance.pk:
|
||||
original = Approval.objects.filter(pk=instance.pk).first()
|
||||
if original:
|
||||
try:
|
||||
original = Approval.objects.get(pk=instance.pk)
|
||||
instance._original_approval_status = original.status
|
||||
except Approval.DoesNotExist:
|
||||
pass
|
||||
|
||||
@ -384,6 +384,19 @@ class UserConsumer(AsyncJsonWebsocketConsumer):
|
||||
"type": "unread_count",
|
||||
"count": event.get("count", 0)
|
||||
})
|
||||
|
||||
# ==================== Обработка событий закупок ====================
|
||||
|
||||
async def procurement_update(self, event):
|
||||
"""
|
||||
Обновление заявки на закупку.
|
||||
Вызывается из procurement/signals.py
|
||||
"""
|
||||
await self.send_json({
|
||||
"type": "procurement_update",
|
||||
"event": event.get("event", "request_updated"),
|
||||
"data": event.get("data", {})
|
||||
})
|
||||
|
||||
# ==================== Обработка действий пользователя ====================
|
||||
|
||||
|
||||
294
backend/tests/procurement/test_integration.py
Normal file
294
backend/tests/procurement/test_integration.py
Normal file
@ -0,0 +1,294 @@
|
||||
"""
|
||||
Тесты интеграции модуля закупок.
|
||||
Сигналы, уведомления, WebSocket события.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from decimal import Decimal
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from notifications.models import (
|
||||
Notification,
|
||||
NotificationCategory,
|
||||
NotificationType,
|
||||
)
|
||||
from procurement.constants import ApprovalRole, ApprovalStatus, ProcurementStatus
|
||||
from procurement.models import (
|
||||
Approval,
|
||||
Budget,
|
||||
ProcurementItem,
|
||||
ProcurementRequest,
|
||||
)
|
||||
from procurement.signals import broadcast_request_update
|
||||
|
||||
Employee = get_user_model()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def notification_setup(db):
|
||||
"""Настройка типов уведомлений для тестов."""
|
||||
category, _ = NotificationCategory.objects.get_or_create(
|
||||
code='procurement',
|
||||
defaults={
|
||||
'name': 'Закупки',
|
||||
'description': 'Уведомления закупок',
|
||||
'icon': 'bi-cart3',
|
||||
'color': '#17a2b8',
|
||||
'order': 60,
|
||||
}
|
||||
)
|
||||
|
||||
notification_types = [
|
||||
'procurement_new_request',
|
||||
'procurement_pending_approval',
|
||||
'procurement_approved',
|
||||
'procurement_rejected',
|
||||
'procurement_completed',
|
||||
'procurement_stage_approved',
|
||||
]
|
||||
|
||||
for type_code in notification_types:
|
||||
NotificationType.objects.get_or_create(
|
||||
code=type_code,
|
||||
defaults={
|
||||
'category': category,
|
||||
'name': type_code.replace('_', ' ').title(),
|
||||
'description': f'Уведомление {type_code}',
|
||||
'priority': 'normal',
|
||||
'default_channels': {
|
||||
'web': True,
|
||||
'email': False,
|
||||
'telegram': False
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return category
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def department_with_head(department_factory, user_factory, link_factory):
|
||||
"""Отдел с руководителем."""
|
||||
dept = department_factory(name="IT отдел")
|
||||
head = user_factory(
|
||||
email="head@test.com",
|
||||
first_name="Руководитель",
|
||||
last_name="Отдела",
|
||||
)
|
||||
head.is_staff = True
|
||||
head.save()
|
||||
dept.head = head
|
||||
dept.save()
|
||||
link_factory(head, dept, is_active=True)
|
||||
return dept
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def requestor(user_factory, department_with_head, link_factory):
|
||||
"""Пользователь-заявитель."""
|
||||
user = user_factory(
|
||||
email="requestor@test.com",
|
||||
first_name="Заявитель",
|
||||
last_name="Тестовый",
|
||||
)
|
||||
link_factory(user, department_with_head, is_active=True)
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def budget_for_department(department_with_head):
|
||||
"""Бюджет для отдела."""
|
||||
from django.utils import timezone
|
||||
now = timezone.now()
|
||||
return Budget.objects.create(
|
||||
department=department_with_head,
|
||||
year=now.year,
|
||||
quarter=(now.month - 1) // 3 + 1,
|
||||
allocated_amount=Decimal('100000.00'),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestProcurementSignals:
|
||||
"""Тесты сигналов модуля закупок."""
|
||||
|
||||
def test_new_request_notifies_department_head(
|
||||
self, notification_setup, department_with_head, requestor
|
||||
):
|
||||
"""Создание заявки уведомляет руководителя отдела."""
|
||||
# Очищаем уведомления
|
||||
Notification.objects.all().delete()
|
||||
|
||||
request = ProcurementRequest.objects.create(
|
||||
title="Тестовая заявка",
|
||||
description="Описание",
|
||||
department=department_with_head,
|
||||
requestor=requestor,
|
||||
estimated_cost=Decimal('5000.00'),
|
||||
)
|
||||
|
||||
# Проверяем уведомление руководителю
|
||||
notifications = Notification.objects.filter(
|
||||
recipient=department_with_head.head
|
||||
)
|
||||
assert notifications.exists()
|
||||
assert 'Новая заявка' in notifications.first().title
|
||||
|
||||
def test_status_change_triggers_notification(
|
||||
self, notification_setup, department_with_head, requestor,
|
||||
budget_for_department
|
||||
):
|
||||
"""Изменение статуса создаёт уведомление."""
|
||||
Notification.objects.all().delete()
|
||||
|
||||
request = ProcurementRequest.objects.create(
|
||||
title="Заявка на согласование",
|
||||
department=department_with_head,
|
||||
requestor=requestor,
|
||||
estimated_cost=Decimal('5000.00'),
|
||||
)
|
||||
|
||||
# Добавляем позицию
|
||||
ProcurementItem.objects.create(
|
||||
request=request,
|
||||
name="Тестовый товар",
|
||||
quantity=1,
|
||||
estimated_unit_price=Decimal('5000.00'),
|
||||
)
|
||||
|
||||
# Создаём согласование
|
||||
Approval.objects.create(
|
||||
request=request,
|
||||
approver=department_with_head.head,
|
||||
role=ApprovalRole.DEPARTMENT_HEAD,
|
||||
)
|
||||
|
||||
# Отправляем на согласование
|
||||
request.status = ProcurementStatus.PENDING
|
||||
request.save()
|
||||
|
||||
# Проверяем уведомление согласующему
|
||||
pending_notifications = Notification.objects.filter(
|
||||
notification_type__code='procurement_pending_approval'
|
||||
)
|
||||
assert pending_notifications.exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestBroadcastFunction:
|
||||
"""Тесты функции broadcast."""
|
||||
|
||||
@patch('procurement.signals.get_channel_layer')
|
||||
def test_broadcast_sends_to_department_group(
|
||||
self, mock_get_channel_layer,
|
||||
notification_setup, department_with_head, requestor
|
||||
):
|
||||
"""broadcast_request_update отправляет в группу отдела."""
|
||||
mock_channel_layer = MagicMock()
|
||||
mock_get_channel_layer.return_value = mock_channel_layer
|
||||
|
||||
request = ProcurementRequest(
|
||||
id=1,
|
||||
title="Тест",
|
||||
status=ProcurementStatus.DRAFT,
|
||||
department=department_with_head,
|
||||
requestor=requestor,
|
||||
)
|
||||
|
||||
broadcast_request_update(request, 'test_event')
|
||||
|
||||
# Проверяем вызов group_send
|
||||
assert mock_channel_layer.group_send.call_count >= 1
|
||||
|
||||
@patch('procurement.signals.get_channel_layer')
|
||||
def test_broadcast_handles_missing_channel_layer(
|
||||
self, mock_get_channel_layer,
|
||||
notification_setup, department_with_head, requestor
|
||||
):
|
||||
"""broadcast_request_update обрабатывает отсутствие channel_layer."""
|
||||
mock_get_channel_layer.return_value = None
|
||||
|
||||
request = ProcurementRequest(
|
||||
id=1,
|
||||
title="Тест",
|
||||
status=ProcurementStatus.DRAFT,
|
||||
department=department_with_head,
|
||||
requestor=requestor,
|
||||
)
|
||||
|
||||
# Не должно вызвать исключение
|
||||
broadcast_request_update(request, 'test_event')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestApprovalNotifications:
|
||||
"""Тесты уведомлений при согласовании."""
|
||||
|
||||
def test_approval_approved_notifies_requestor(
|
||||
self, notification_setup, department_with_head, requestor,
|
||||
budget_for_department
|
||||
):
|
||||
"""Одобрение согласования уведомляет заявителя."""
|
||||
Notification.objects.all().delete()
|
||||
|
||||
request = ProcurementRequest.objects.create(
|
||||
title="Заявка",
|
||||
department=department_with_head,
|
||||
requestor=requestor,
|
||||
status=ProcurementStatus.PENDING,
|
||||
estimated_cost=Decimal('5000.00'),
|
||||
)
|
||||
|
||||
approval = Approval.objects.create(
|
||||
request=request,
|
||||
approver=department_with_head.head,
|
||||
role=ApprovalRole.DEPARTMENT_HEAD,
|
||||
status=ApprovalStatus.PENDING,
|
||||
)
|
||||
|
||||
# Одобряем
|
||||
approval.status = ApprovalStatus.APPROVED
|
||||
approval.save()
|
||||
|
||||
# Проверяем уведомление заявителю
|
||||
stage_notifications = Notification.objects.filter(
|
||||
recipient=requestor,
|
||||
notification_type__code='procurement_stage_approved'
|
||||
)
|
||||
assert stage_notifications.exists()
|
||||
|
||||
def test_approval_rejected_notifies_requestor(
|
||||
self, notification_setup, department_with_head, requestor,
|
||||
budget_for_department
|
||||
):
|
||||
"""Отклонение согласования уведомляет заявителя."""
|
||||
Notification.objects.all().delete()
|
||||
|
||||
request = ProcurementRequest.objects.create(
|
||||
title="Заявка",
|
||||
department=department_with_head,
|
||||
requestor=requestor,
|
||||
status=ProcurementStatus.PENDING,
|
||||
estimated_cost=Decimal('5000.00'),
|
||||
)
|
||||
|
||||
approval = Approval.objects.create(
|
||||
request=request,
|
||||
approver=department_with_head.head,
|
||||
role=ApprovalRole.DEPARTMENT_HEAD,
|
||||
status=ApprovalStatus.PENDING,
|
||||
)
|
||||
|
||||
# Отклоняем
|
||||
approval.status = ApprovalStatus.REJECTED
|
||||
approval.comment = "Недостаточно обоснования"
|
||||
approval.save()
|
||||
|
||||
# Проверяем уведомление заявителю
|
||||
reject_notifications = Notification.objects.filter(
|
||||
recipient=requestor,
|
||||
notification_type__code='procurement_rejected'
|
||||
)
|
||||
assert reject_notifications.exists()
|
||||
Loading…
x
Reference in New Issue
Block a user