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).
230 lines
8.6 KiB
Python
230 lines
8.6 KiB
Python
"""
|
||
Сигналы для модуля закупок.
|
||
Автоматическая отправка уведомлений при изменении статусов.
|
||
WebSocket события для real-time обновлений.
|
||
"""
|
||
|
||
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
|
||
|
||
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,
|
||
notification_type_code='procurement_new_request',
|
||
title="Новая заявка на закупку",
|
||
message=(
|
||
f'Создана заявка "{instance.title}" '
|
||
f'на сумму {instance.estimated_cost}₽'
|
||
),
|
||
action_url=f'/procurement/requests/{instance.id}/',
|
||
send_immediately=True,
|
||
)
|
||
return
|
||
|
||
# Проверяем изменение статуса
|
||
if hasattr(instance, '_original_status'):
|
||
old_status = instance._original_status
|
||
new_status = instance.status
|
||
|
||
if old_status == new_status:
|
||
return
|
||
|
||
# Broadcast обновления статуса
|
||
broadcast_request_update(instance, 'request_status_changed')
|
||
|
||
# Статус изменился
|
||
if new_status == ProcurementStatus.PENDING:
|
||
# Заявка отправлена на согласование
|
||
notify_approvers(instance)
|
||
|
||
elif new_status == ProcurementStatus.APPROVED:
|
||
# Заявка одобрена
|
||
notify_requestor(
|
||
instance,
|
||
'procurement_approved',
|
||
"Заявка одобрена",
|
||
f'Ваша заявка "{instance.title}" была одобрена. '
|
||
f'Можно приступать к закупке.'
|
||
)
|
||
|
||
elif new_status == ProcurementStatus.REJECTED:
|
||
# Заявка отклонена
|
||
notify_requestor(
|
||
instance,
|
||
'procurement_rejected',
|
||
"Заявка отклонена",
|
||
f'Ваша заявка "{instance.title}" была отклонена. '
|
||
f'Проверьте комментарии согласующих.'
|
||
)
|
||
|
||
elif new_status == ProcurementStatus.COMPLETED:
|
||
# Заявка завершена
|
||
notify_requestor(
|
||
instance,
|
||
'procurement_completed',
|
||
"Заявка завершена",
|
||
f'Закупка по заявке "{instance.title}" завершена.'
|
||
)
|
||
|
||
|
||
@receiver(post_save, sender=Approval)
|
||
def notify_on_approval_change(sender, instance, created, **kwargs):
|
||
"""Отправить уведомление при изменении согласования."""
|
||
if created:
|
||
# Новое согласование создано
|
||
notify_approver(instance)
|
||
return
|
||
|
||
# Проверяем изменение статуса согласования
|
||
if hasattr(instance, '_original_approval_status'):
|
||
old_status = instance._original_approval_status
|
||
new_status = instance.status
|
||
|
||
if old_status == new_status:
|
||
return
|
||
|
||
# Статус согласования изменился
|
||
if new_status == ApprovalStatus.APPROVED:
|
||
# Согласование одобрено
|
||
notify_requestor(
|
||
instance.request,
|
||
'procurement_stage_approved',
|
||
"Этап согласования пройден",
|
||
f'{instance.approver.get_full_name()} одобрил '
|
||
f'заявку "{instance.request.title}".'
|
||
)
|
||
|
||
elif new_status == ApprovalStatus.REJECTED:
|
||
# Согласование отклонено
|
||
notify_requestor(
|
||
instance.request,
|
||
'procurement_rejected',
|
||
"Заявка отклонена",
|
||
f'{instance.approver.get_full_name()} отклонил '
|
||
f'заявку "{instance.request.title}". '
|
||
f'Причина: {instance.comment}'
|
||
)
|
||
|
||
|
||
def notify_approvers(request):
|
||
"""Уведомить всех согласующих о новой заявке."""
|
||
for approval in request.approvals.filter(
|
||
status=ApprovalStatus.PENDING
|
||
):
|
||
notify_approver(approval)
|
||
|
||
|
||
def notify_approver(approval):
|
||
"""Уведомить согласующего о необходимости согласования."""
|
||
NotificationService.create_notification(
|
||
recipient=approval.approver,
|
||
notification_type_code='procurement_pending_approval',
|
||
title="Требуется согласование",
|
||
message=(
|
||
f'Заявка "{approval.request.title}" '
|
||
f'ожидает вашего согласования. '
|
||
f'Сумма: {approval.request.estimated_cost}₽'
|
||
),
|
||
action_url=f'/procurement/requests/{approval.request.id}/',
|
||
send_immediately=True,
|
||
)
|
||
|
||
|
||
def notify_requestor(request, notification_type_code, title, message):
|
||
"""Уведомить создателя заявки."""
|
||
NotificationService.create_notification(
|
||
recipient=request.requestor,
|
||
notification_type_code=notification_type_code,
|
||
title=title,
|
||
message=message,
|
||
action_url=f'/procurement/requests/{request.id}/',
|
||
send_immediately=True,
|
||
)
|
||
|
||
|
||
# Сохраняем исходный статус перед сохранением
|
||
@receiver(pre_save, sender=ProcurementRequest)
|
||
def save_original_status(sender, instance, **kwargs):
|
||
"""Сохранить оригинальный статус для отслеживания изменений."""
|
||
if instance.pk:
|
||
try:
|
||
original = ProcurementRequest.objects.get(pk=instance.pk)
|
||
instance._original_status = original.status
|
||
except ProcurementRequest.DoesNotExist:
|
||
pass
|
||
|
||
|
||
@receiver(pre_save, sender=Approval)
|
||
def save_original_approval_status(sender, instance, **kwargs):
|
||
"""Сохранить оригинальный статус согласования."""
|
||
if instance.pk:
|
||
try:
|
||
original = Approval.objects.get(pk=instance.pk)
|
||
instance._original_approval_status = original.status
|
||
except Approval.DoesNotExist:
|
||
pass
|