refactor(procurement): реструктуризация notifications в модульную архитектуру

- Создана папка backend/procurement/notifications/ вместо одного signals.py
- Разделена логика на специализированные модули:
  * config.py - типы уведомлений (verbs) и шаблоны сообщений
  * handlers.py - бизнес-логика отправки уведомлений
  * signals.py - только Django сигналы
  * websocket.py - WebSocket broadcast для real-time обновлений
  * __init__.py - публичный API модуля

Преимущества новой структуры:
- ✅ Четкое разделение ответственности (SRP)
- ✅ WebSocket логика отделена от уведомлений
- ✅ Все шаблоны сообщений в одном месте (MessageTemplates)
- ✅ Константы verbs централизованы (NotificationVerbs)
- ✅ Легче тестировать и расширять
- ✅ Обратная совместимость через __init__.py

Изменения:
- Удален: signals.py (сохранен как .old для истории)
- Добавлен: notifications/ модуль с 5 файлами
- Обновлен: apps.py (новый импорт сигналов)
This commit is contained in:
lizerk 2026-03-10 12:34:05 +03:00
parent 3583aee95a
commit 16c5f2023a
7 changed files with 889 additions and 306 deletions

View File

@ -8,5 +8,6 @@ class ProcurementConfig(AppConfig):
def ready(self):
"""Подключение сигналов при загрузке приложения."""
import procurement.signals # noqa: F401
import procurement.rules # django-rules: регистрация предикатов и правил доступа
import procurement.notifications.signals # noqa: F401
# django-rules: регистрация предикатов и правил доступа
import procurement.rules # noqa: F401

View File

@ -0,0 +1,80 @@
"""
Модуль уведомлений для приложения Procurement.
Обеспечивает автоматическую отправку уведомлений при:
- Создании новой заявки на закупку
- Изменении статуса заявки
- Создании/изменении согласования
- WebSocket broadcast для real-time обновлений
Использует универсальную систему уведомлений (backend/notifications).
Структура:
- config.py - конфигурация (типы уведомлений, шаблоны, URLs)
- handlers.py - бизнес-логика отправки уведомлений
- signals.py - Django сигналы для автоматической генерации
- websocket.py - WebSocket broadcast для real-time уведомлений
Usage:
# Сигналы подключаются автоматически через AppConfig.ready()
# Для ручной отправки:
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')
"""
# Импорты для удобства и обратной совместимости
from .config import NotificationVerbs, MessageTemplates, ActionURLs
from .handlers import (
notify_new_request,
notify_approvers,
notify_approver,
notify_requestor,
notify_request_approved,
notify_request_rejected,
notify_request_completed,
notify_request_in_progress,
notify_request_cancelled,
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
__all__ = [
# Config
'NotificationVerbs',
'MessageTemplates',
'ActionURLs',
# Handlers
'notify_new_request',
'notify_approvers',
'notify_approver',
'notify_requestor',
'notify_request_approved',
'notify_request_rejected',
'notify_request_completed',
'notify_request_in_progress',
'notify_request_cancelled',
'notify_stage_approved',
'notify_stage_rejected',
# WebSocket
'broadcast_request_update',
'broadcast_request_created',
'broadcast_request_status_changed',
# Signals module (для импорта в AppConfig)
'signals',
]

View File

@ -0,0 +1,211 @@
"""
Конфигурация уведомлений модуля Procurement.
Определяет типы уведомлений (verbs), шаблоны сообщений и URLs.
"""
# ===== Типы уведомлений (Verbs) =====
class NotificationVerbs:
"""Типы уведомлений для заявок на закупку."""
# Новая заявка
NEW_REQUEST = 'procurement_new_request'
# Согласование
PENDING_APPROVAL = 'procurement_pending_approval'
STAGE_APPROVED = 'procurement_stage_approved'
# Статусы
APPROVED = 'procurement_approved'
REJECTED = 'procurement_rejected'
IN_PROGRESS = 'procurement_in_progress'
COMPLETED = 'procurement_completed'
CANCELLED = 'procurement_cancelled'
# ===== Шаблоны сообщений =====
class MessageTemplates:
"""Шаблоны сообщений для уведомлений."""
@staticmethod
def new_request(title: str, total_cost: float) -> tuple[str, str]:
"""
Шаблон для новой заявки.
Returns:
tuple: (notification_title, description)
"""
notification_title = 'Новая заявка на закупку'
description = f'Создана заявка "{title}" на сумму {total_cost}₽'
return notification_title, description
@staticmethod
def pending_approval(
title: str, total_cost: float
) -> tuple[str, str]:
"""
Шаблон для запроса на согласование.
Returns:
tuple: (notification_title, description)
"""
notification_title = 'Требуется согласование'
description = (
f'Заявка "{title}" ожидает вашего согласования. '
f'Сумма: {total_cost}₽'
)
return notification_title, description
@staticmethod
def stage_approved(approver_name: str, title: str) -> tuple[str, str]:
"""
Шаблон для одобрения этапа согласования.
Returns:
tuple: (notification_title, description)
"""
notification_title = 'Этап согласования пройден'
description = f'{approver_name} одобрил заявку "{title}".'
return notification_title, description
@staticmethod
def approved(title: str) -> tuple[str, str]:
"""
Шаблон для полного одобрения заявки.
Returns:
tuple: (notification_title, description)
"""
notification_title = 'Заявка одобрена'
description = (
f'Ваша заявка "{title}" была одобрена. '
f'Можно приступать к закупке.'
)
return notification_title, description
@staticmethod
def rejected(title: str, comment: str = None) -> tuple[str, str]:
"""
Шаблон для отклонения заявки.
Returns:
tuple: (notification_title, description)
"""
notification_title = 'Заявка отклонена'
if comment:
description = (
f'Ваша заявка "{title}" была отклонена. '
f'Причина: {comment}'
)
else:
description = (
f'Ваша заявка "{title}" была отклонена. '
f'Проверьте комментарии согласующих.'
)
return notification_title, description
@staticmethod
def rejected_by_approver(
approver_name: str, title: str, comment: str
) -> tuple[str, str]:
"""
Шаблон для отклонения конкретным согласующим.
Returns:
tuple: (notification_title, description)
"""
notification_title = 'Заявка отклонена'
description = (
f'{approver_name} отклонил заявку "{title}". '
f'Причина: {comment}'
)
return notification_title, description
@staticmethod
def in_progress(
title: str, executor_name: str
) -> tuple[str, str]:
"""
Шаблон для взятия заявки в работу.
Returns:
tuple: (notification_title, description)
"""
notification_title = 'Заявка взята в работу'
description = (
f'Заявка "{title}" взята в работу '
f'пользователем {executor_name}.'
)
return notification_title, description
@staticmethod
def in_progress_requestor(
title: str, executor_name: str
) -> tuple[str, str]:
"""
Шаблон для создателя заявки о взятии в работу.
Returns:
tuple: (notification_title, description)
"""
notification_title = 'Ваша заявка взята в работу'
description = (
f'Заявка "{title}" взята в работу '
f'пользователем {executor_name}.'
)
return notification_title, description
@staticmethod
def completed(title: str) -> tuple[str, str]:
"""
Шаблон для завершения заявки.
Returns:
tuple: (notification_title, description)
"""
notification_title = 'Заявка завершена'
description = f'Закупка по заявке "{title}" завершена.'
return notification_title, description
@staticmethod
def completed_approver(title: str) -> tuple[str, str]:
"""
Шаблон для согласующих о завершении заявки.
Returns:
tuple: (notification_title, description)
"""
notification_title = 'Заявка завершена'
description = f'Заявка "{title}" успешно завершена.'
return notification_title, description
@staticmethod
def cancelled(title: str, reason: str = 'не указана') -> tuple[str, str]:
"""
Шаблон для отмены заявки.
Returns:
tuple: (notification_title, description)
"""
notification_title = 'Заявка отменена'
description = (
f'Заявка "{title}" была отменена автором. '
f'Причина: {reason}'
)
return notification_title, description
# ===== URL для действий =====
class ActionURLs:
"""URL для переходов из уведомлений."""
PROCUREMENT_LIST = '/procurement'
@staticmethod
def request_detail(request_id: int) -> str:
"""URL детальной страницы заявки."""
return f'/procurement/requests/{request_id}'

View File

@ -0,0 +1,340 @@
"""
Обработчики (handlers) для отправки уведомлений о заявках на закупку.
Содержит бизнес-логику формирования и отправки уведомлений
для различных событий в модуле Procurement.
"""
import logging
from notifications.signals import notify
from .config import NotificationVerbs, MessageTemplates, ActionURLs
logger = logging.getLogger(__name__)
def notify_new_request(request):
"""
Уведомить о новой заявке на закупку.
Отправляет уведомление руководителю отдела.
Args:
request: Объект ProcurementRequest
"""
if not request.department.head:
logger.warning(
f"[Procurement] Нет руководителя для отдела {request.department}"
)
return
notification_title, description = MessageTemplates.new_request(
request.title, request.total_cost
)
notify.send(
sender=None,
recipient=request.department.head,
verb=NotificationVerbs.NEW_REQUEST,
action_object=request,
description=description,
action_url=ActionURLs.PROCUREMENT_LIST,
data={
'title': notification_title,
'request_id': request.id,
'total_cost': float(request.total_cost),
},
)
logger.info(
f"[Procurement] Отправлено уведомление о новой заявке #{request.id} "
f"руководителю {request.department.head.username}"
)
def notify_approvers(request):
"""
Уведомить всех согласующих о необходимости согласования заявки.
Args:
request: Объект ProcurementRequest
"""
from ..constants import ApprovalStatus
pending_approvals = request.approvals.filter(
status=ApprovalStatus.PENDING
)
if not pending_approvals.exists():
logger.warning(
f"[Procurement] Нет ожидающих согласований "
f"для заявки #{request.id}"
)
return
for approval in pending_approvals:
notify_approver(approval)
def notify_approver(approval):
"""
Уведомить конкретного согласующего о необходимости согласования.
Args:
approval: Объект Approval
"""
notification_title, description = MessageTemplates.pending_approval(
approval.request.title, approval.request.total_cost
)
notify.send(
sender=None,
recipient=approval.approver,
verb=NotificationVerbs.PENDING_APPROVAL,
action_object=approval.request,
description=description,
action_url=ActionURLs.PROCUREMENT_LIST,
data={
'title': notification_title,
'request_id': approval.request.id,
'approval_id': approval.id,
'total_cost': float(approval.request.total_cost),
},
)
logger.info(
f"[Procurement] Отправлено уведомление о согласовании "
f"заявки #{approval.request.id} для {approval.approver.username}"
)
def notify_requestor(request, verb, title, message):
"""
Уведомить создателя заявки.
Args:
request: Объект ProcurementRequest
verb: Тип уведомления (из NotificationVerbs)
title: Заголовок уведомления
message: Текст сообщения
"""
notify.send(
sender=None,
recipient=request.requestor,
verb=verb,
action_object=request,
description=message,
action_url=ActionURLs.PROCUREMENT_LIST,
data={
'title': title,
'request_id': request.id,
},
)
logger.info(
f"[Procurement] Отправлено уведомление '{verb}' "
f"создателю заявки #{request.id}"
)
def notify_request_approved(request):
"""
Уведомить о полном одобрении заявки.
Args:
request: Объект ProcurementRequest
"""
notification_title, description = MessageTemplates.approved(request.title)
notify_requestor(
request,
NotificationVerbs.APPROVED,
notification_title,
description
)
def notify_request_rejected(request):
"""
Уведомить об отклонении заявки.
Args:
request: Объект ProcurementRequest
"""
notification_title, description = MessageTemplates.rejected(request.title)
notify_requestor(
request,
NotificationVerbs.REJECTED,
notification_title,
description
)
def notify_request_completed(request):
"""
Уведомить о завершении заявки.
Отправляет уведомления:
- Создателю заявки
- Всем одобрившим согласующим
Args:
request: Объект ProcurementRequest
"""
from ..constants import ApprovalStatus
# Уведомляем создателя
notification_title, description = MessageTemplates.completed(request.title)
notify_requestor(
request,
NotificationVerbs.COMPLETED,
notification_title,
description
)
# Уведомляем всех одобривших согласующих
notification_title, description = MessageTemplates.completed_approver(
request.title
)
for approval in request.approvals.filter(status=ApprovalStatus.APPROVED):
notify.send(
sender=None,
recipient=approval.approver,
verb=NotificationVerbs.COMPLETED,
action_object=request,
description=description,
action_url=ActionURLs.PROCUREMENT_LIST,
data={
'title': notification_title,
'request_id': request.id,
},
)
def notify_request_in_progress(request, executor):
"""
Уведомить о взятии заявки в работу.
Отправляет уведомления:
- Создателю заявки (если он не сам взял в работу)
- Всем одобрившим согласующим
Args:
request: Объект ProcurementRequest
executor: Пользователь, взявший заявку в работу
"""
from ..constants import ApprovalStatus
executor_name = executor.get_full_name() if executor else 'Сотрудник'
# Уведомляем создателя (если он не сам взял в работу)
if executor and request.requestor != executor:
notification_title, description = (
MessageTemplates.in_progress_requestor(
request.title, executor_name
)
)
notify.send(
sender=executor,
recipient=request.requestor,
verb=NotificationVerbs.IN_PROGRESS,
action_object=request,
description=description,
action_url=ActionURLs.PROCUREMENT_LIST,
data={
'title': notification_title,
'request_id': request.id,
'executor_id': executor.id,
},
)
# Уведомляем всех одобривших согласующих
notification_title, description = MessageTemplates.in_progress(
request.title, executor_name
)
for approval in request.approvals.filter(status=ApprovalStatus.APPROVED):
notify.send(
sender=executor,
recipient=approval.approver,
verb=NotificationVerbs.IN_PROGRESS,
action_object=request,
description=description,
action_url=ActionURLs.PROCUREMENT_LIST,
data={
'title': notification_title,
'request_id': request.id,
'executor_id': executor.id if executor else None,
},
)
def notify_request_cancelled(request):
"""
Уведомить об отмене заявки.
Отправляет уведомления всем причастным согласующим.
Args:
request: Объект ProcurementRequest
"""
reason = getattr(request, 'cancellation_reason', 'не указана')
notification_title, description = MessageTemplates.cancelled(
request.title, reason
)
for approval in request.approvals.all():
notify.send(
sender=None,
recipient=approval.approver,
verb=NotificationVerbs.CANCELLED,
action_object=request,
description=description,
action_url=ActionURLs.PROCUREMENT_LIST,
data={
'title': notification_title,
'request_id': request.id,
'reason': reason,
},
)
def notify_stage_approved(approval):
"""
Уведомить о прохождении этапа согласования.
Args:
approval: Объект Approval
"""
approver_name = approval.approver.get_full_name()
notification_title, description = MessageTemplates.stage_approved(
approver_name, approval.request.title
)
notify_requestor(
approval.request,
NotificationVerbs.STAGE_APPROVED,
notification_title,
description
)
def notify_stage_rejected(approval):
"""
Уведомить об отклонении на этапе согласования.
Args:
approval: Объект Approval
"""
approver_name = approval.approver.get_full_name()
notification_title, description = MessageTemplates.rejected_by_approver(
approver_name, approval.request.title, approval.comment
)
notify_requestor(
approval.request,
NotificationVerbs.REJECTED,
notification_title,
description
)

View File

@ -0,0 +1,166 @@
"""
Django сигналы для автоматической генерации уведомлений в модуле Procurement.
Обрабатывает события:
- Создание новой заявки на закупку
- Изменение статуса заявки
- Изменение статуса согласования
"""
import logging
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver
from ..constants import ApprovalStatus, ProcurementStatus
from ..models import Approval, ProcurementRequest
from .handlers import (
notify_new_request,
notify_approvers,
notify_request_approved,
notify_request_rejected,
notify_request_completed,
notify_request_in_progress,
notify_request_cancelled,
notify_stage_approved,
notify_stage_rejected,
)
from .websocket import (
broadcast_request_created,
broadcast_request_status_changed,
)
logger = logging.getLogger(__name__)
@receiver(post_save, sender=ProcurementRequest)
def create_request_notifications(sender, instance, created, **kwargs):
"""
Создает уведомления при создании или изменении заявки.
Обрабатывает:
1. Новая заявка - уведомление руководителю отдела
2. Изменение статуса - уведомления соответствующим получателям
"""
try:
request = instance
if created:
# Новая заявка создана
broadcast_request_created(request)
notify_new_request(request)
return
# Проверяем изменение статуса
if hasattr(request, '_original_status'):
old_status = request._original_status
new_status = request.status
if old_status == new_status:
return
# Broadcast обновления статуса
broadcast_request_status_changed(request)
# Обработка изменения статуса
_handle_status_change(request, new_status)
except Exception as e:
logger.exception(f"[SIGNAL ERROR] create_request_notifications: {e}")
def _handle_status_change(request, new_status):
"""
Обработать изменение статуса заявки.
Args:
request: Объект ProcurementRequest
new_status: Новый статус
"""
if new_status == ProcurementStatus.PENDING:
# Заявка отправлена на согласование
notify_approvers(request)
elif new_status == ProcurementStatus.APPROVED:
# Заявка одобрена
notify_request_approved(request)
elif new_status == ProcurementStatus.REJECTED:
# Заявка отклонена
notify_request_rejected(request)
elif new_status == ProcurementStatus.COMPLETED:
# Заявка завершена
notify_request_completed(request)
elif new_status == ProcurementStatus.IN_PROGRESS:
# Заявка взята в работу
executor = request.executor
notify_request_in_progress(request, executor)
elif new_status == ProcurementStatus.CANCELLED:
# Заявка отменена
notify_request_cancelled(request)
@receiver(post_save, sender=Approval)
def create_approval_notifications(sender, instance, created, **kwargs):
"""
Создает уведомления при создании или изменении согласования.
Обрабатывает:
1. Новое согласование - уведомление согласующему
2. Одобрение - уведомление создателю заявки
3. Отклонение - уведомление создателю заявки
"""
try:
approval = instance
if created:
# Новое согласование создано
from .handlers import notify_approver
notify_approver(approval)
return
# Проверяем изменение статуса согласования
if hasattr(approval, '_original_approval_status'):
old_status = approval._original_approval_status
new_status = approval.status
if old_status == new_status:
return
# Обработка изменения статуса согласования
if new_status == ApprovalStatus.APPROVED:
notify_stage_approved(approval)
elif new_status == ApprovalStatus.REJECTED:
notify_stage_rejected(approval)
except Exception as e:
logger.exception(f"[SIGNAL ERROR] create_approval_notifications: {e}")
@receiver(pre_save, sender=ProcurementRequest)
def track_request_status_change(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 track_approval_status_change(sender, instance, **kwargs):
"""
Сохранить оригинальный статус согласования для отслеживания изменений.
"""
if instance.pk:
try:
original = Approval.objects.get(pk=instance.pk)
instance._original_approval_status = original.status
except Approval.DoesNotExist:
pass

View File

@ -0,0 +1,89 @@
"""
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

@ -1,304 +0,0 @@
"""
Сигналы для модуля закупок.
Автоматическая отправка уведомлений при изменении статусов.
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