refactor(procurement): удаление дублирующего WebSocket модуля

Проблема:
- procurement/notifications/websocket.py дублировал функциональность
- Прямые вызовы channel_layer.group_send() обходили универсальную систему
- Нарушение единообразия использования notifications

Решение:
- Удален websocket.py модуль
- WebSocket уведомления теперь отправляются через универсальную систему:
  notify.send() → channels.py → Celery → WebSocketNotificationSender

Преимущества:
- ✅ Единообразие архитектуры (как в requests_app)
- ✅ Все каналы (WebSocket, Email, Push) через одну систему
- ✅ Автоматический retry и rate limiting
- ✅ Централизованное логирование
- ✅ Меньше кода для поддержки

Изменения:
- Удален: notifications/websocket.py
- Обновлен: notifications/signals.py (удалены broadcast вызовы)
- Обновлен: notifications/__init__.py (удалены импорты websocket)
This commit is contained in:
lizerk 2026-03-10 12:37:49 +03:00
parent 16c5f2023a
commit 46e7059a40
4 changed files with 312 additions and 112 deletions

View File

@ -5,15 +5,14 @@
- Создании новой заявки на закупку
- Изменении статуса заявки
- Создании/изменении согласования
- WebSocket broadcast для real-time обновлений
Использует универсальную систему уведомлений (backend/notifications).
Все уведомления (включая WebSocket) отправляются через notify.send().
Структура:
- config.py - конфигурация (типы уведомлений, шаблоны, URLs)
- handlers.py - бизнес-логика отправки уведомлений
- signals.py - Django сигналы для автоматической генерации
- websocket.py - WebSocket broadcast для real-time уведомлений
Usage:
# Сигналы подключаются автоматически через AppConfig.ready()
@ -21,9 +20,8 @@ Usage:
from procurement.notifications import notify_new_request
notify_new_request(request_obj)
# Для WebSocket broadcast:
from procurement.notifications import broadcast_request_update
broadcast_request_update(request_obj, 'request_updated')
# WebSocket broadcast происходит автоматически через:
# notify.send() → channels.py → Celery → WebSocketNotificationSender
"""
# Импорты для удобства и обратной совместимости
@ -41,11 +39,6 @@ from .handlers import (
notify_stage_approved,
notify_stage_rejected,
)
from .websocket import (
broadcast_request_update,
broadcast_request_created,
broadcast_request_status_changed,
)
# Сигналы импортируются для регистрации через AppConfig.ready()
from . import signals # noqa: F401
@ -70,11 +63,6 @@ __all__ = [
'notify_stage_approved',
'notify_stage_rejected',
# WebSocket
'broadcast_request_update',
'broadcast_request_created',
'broadcast_request_status_changed',
# Signals module (для импорта в AppConfig)
'signals',
]

View File

@ -5,6 +5,9 @@ Django сигналы для автоматической генерации у
- Создание новой заявки на закупку
- Изменение статуса заявки
- Изменение статуса согласования
Все уведомления отправляются через универсальную систему notifications:
noti fy.send() → channels.py → Celery → WebSocket/Email/Push
"""
import logging
@ -24,10 +27,6 @@ from .handlers import (
notify_stage_approved,
notify_stage_rejected,
)
from .websocket import (
broadcast_request_created,
broadcast_request_status_changed,
)
logger = logging.getLogger(__name__)
@ -46,7 +45,7 @@ def create_request_notifications(sender, instance, created, **kwargs):
if created:
# Новая заявка создана
broadcast_request_created(request)
# WebSocket broadcast будет автоматически через notify.send()
notify_new_request(request)
return
@ -58,10 +57,8 @@ def create_request_notifications(sender, instance, created, **kwargs):
if old_status == new_status:
return
# Broadcast обновления статуса
broadcast_request_status_changed(request)
# Обработка изменения статуса
# WebSocket broadcast будет автоматически через notify.send()
_handle_status_change(request, new_status)
except Exception as e:

View File

@ -1,89 +0,0 @@
"""
WebSocket broadcast для real-time уведомлений в модуле Procurement.
Отправляет события через Django Channels о изменениях заявок на закупку.
"""
import logging
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
logger = logging.getLogger(__name__)
def broadcast_request_update(request, event_type='request_updated'):
"""
Отправить WebSocket событие об обновлении заявки.
Отправляет уведомления:
- Всем пользователям отдела
- Создателю заявки
Args:
request: Объект ProcurementRequest
event_type: Тип события (request_created, request_status_changed, etc.)
"""
try:
channel_layer = get_channel_layer()
if not channel_layer:
logger.debug("[Procurement WS] 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} "
f"для заявки #{request.id}"
)
except Exception as e:
logger.warning(f"[Procurement WS] Broadcast failed: {e}")
def broadcast_request_created(request):
"""
Broadcast о создании новой заявки.
Args:
request: Объект ProcurementRequest
"""
broadcast_request_update(request, 'request_created')
def broadcast_request_status_changed(request):
"""
Broadcast об изменении статуса заявки.
Args:
request: Объект ProcurementRequest
"""
broadcast_request_update(request, 'request_status_changed')

View File

@ -0,0 +1,304 @@
"""
Сигналы для модуля закупок.
Автоматическая отправка уведомлений при изменении статусов.
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.signals import notify
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:
notify.send(
sender=None,
recipient=instance.department.head,
verb='procurement_new_request',
action_object=instance,
description=(
f'Создана заявка "{instance.title}" '
f'на сумму {instance.total_cost}₽'
),
action_url='/procurement',
data={'title': 'Новая заявка на закупку'},
)
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}" завершена.'
)
# Уведомляем всех одобривших согласующих
for approval in instance.approvals.filter(status=ApprovalStatus.APPROVED):
notify.send(
sender=None,
recipient=approval.approver,
verb='procurement_completed',
action_object=instance,
description=f'Заявка "{instance.title}" успешно завершена.',
action_url='/procurement',
data={'title': 'Заявка завершена'},
)
elif new_status == ProcurementStatus.IN_PROGRESS:
# Заявка взята в работу
executor = instance.executor
executor_name = executor.get_full_name() if executor else 'Сотрудник'
# Уведомляем создателя заявки (если он не сам взял в работу)
if executor and instance.requestor != executor:
notify.send(
sender=executor,
recipient=instance.requestor,
verb='procurement_in_progress',
action_object=instance,
description=(
f'Заявка "{instance.title}" взята в работу '
f'пользователем {executor_name}.'
),
action_url='/procurement',
data={'title': 'Ваша заявка взята в работу'},
)
# Уведомляем всех одобривших согласующих
for approval in instance.approvals.filter(status=ApprovalStatus.APPROVED):
notify.send(
sender=executor,
recipient=approval.approver,
verb='procurement_in_progress',
action_object=instance,
description=(
f'Заявка "{instance.title}" взята в работу '
f'пользователем {executor_name}.'
),
action_url='/procurement',
data={'title': 'Заявка взята в работу'},
)
elif new_status == ProcurementStatus.CANCELLED:
# Заявка отменена - уведомляем всех причастных согласующих
reason = getattr(instance, 'cancellation_reason', 'не указана')
for approval in instance.approvals.all():
notify.send(
sender=None,
recipient=approval.approver,
verb='procurement_cancelled',
action_object=instance,
description=(
f'Заявка "{instance.title}" была отменена автором. '
f'Причина: {reason}'
),
action_url='/procurement',
data={'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):
"""Уведомить согласующего о необходимости согласования."""
notify.send(
sender=None,
recipient=approval.approver,
verb='procurement_pending_approval',
action_object=approval.request,
description=(
f'Заявка "{approval.request.title}" '
f'ожидает вашего согласования. '
f'Сумма: {approval.request.total_cost}₽'
),
action_url='/procurement',
data={'title': 'Требуется согласование'},
)
def notify_requestor(request, verb, title, message):
"""Уведомить создателя заявки."""
notify.send(
sender=None,
recipient=request.requestor,
verb=verb,
action_object=request,
description=message,
action_url='/procurement',
data={'title': title},
)
# Сохраняем исходный статус перед сохранением
@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
@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