Refine procurement notifications
This commit is contained in:
parent
31b91fe15c
commit
b16b74c060
@ -230,7 +230,7 @@ class CanManageProcurementRequest(permissions.BasePermission):
|
||||
if action == "complete":
|
||||
return obj.executor == user
|
||||
|
||||
if action == "mark_all_received":
|
||||
if action in ("mark_all_received", "notify_arrival"):
|
||||
if obj.executor == user:
|
||||
return True
|
||||
if obj.processing_department_id:
|
||||
|
||||
@ -32,6 +32,7 @@ from procurement.notifications.handlers import (
|
||||
notify_item_comment,
|
||||
notify_item_issue_reported,
|
||||
notify_item_updated,
|
||||
notify_request_arrival,
|
||||
notify_request_comment,
|
||||
)
|
||||
from communications import comments_helpers
|
||||
@ -897,6 +898,31 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
|
||||
serializer = self.get_serializer(procurement_request)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=["post"])
|
||||
def notify_arrival(self, request, pk=None):
|
||||
"""Вручную уведомить заказчика о поступлении по заявке."""
|
||||
procurement_request = self.get_object()
|
||||
self.check_object_permissions(request, procurement_request)
|
||||
|
||||
if procurement_request.status not in [
|
||||
ProcurementStatus.WAITING,
|
||||
ProcurementStatus.IN_PROGRESS,
|
||||
]:
|
||||
return Response(
|
||||
{
|
||||
"error": (
|
||||
"Уведомить о поступлении можно только по ожидающей "
|
||||
"или взятой в работу заявке"
|
||||
)
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
notify_request_arrival(procurement_request, actor=request.user)
|
||||
|
||||
serializer = self.get_serializer(procurement_request)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=["post"])
|
||||
def set_viewed(self, request, pk=None):
|
||||
"""Установить отметку просмотра заявки."""
|
||||
|
||||
@ -35,6 +35,7 @@ from .handlers import (
|
||||
notify_request_approved,
|
||||
notify_request_rejected,
|
||||
notify_request_completed,
|
||||
notify_request_arrival,
|
||||
notify_request_in_progress,
|
||||
notify_executor_reassigned,
|
||||
notify_request_returned_to_processing,
|
||||
@ -66,6 +67,7 @@ __all__ = [
|
||||
'notify_request_approved',
|
||||
'notify_request_rejected',
|
||||
'notify_request_completed',
|
||||
'notify_request_arrival',
|
||||
'notify_request_in_progress',
|
||||
'notify_executor_reassigned',
|
||||
'notify_request_returned_to_processing',
|
||||
|
||||
@ -23,6 +23,7 @@ class NotificationVerbs:
|
||||
REJECTED = 'procurement_rejected'
|
||||
IN_PROGRESS = 'procurement_in_progress'
|
||||
EXECUTOR_REASSIGNED = 'procurement_executor_reassigned'
|
||||
ARRIVAL_NOTICE = 'procurement_arrival_notice'
|
||||
COMPLETED = 'procurement_completed'
|
||||
CANCELLED = 'procurement_cancelled'
|
||||
|
||||
@ -210,6 +211,15 @@ class MessageTemplates:
|
||||
description = f'Заявка "{title}" успешно завершена.'
|
||||
return notification_title, description
|
||||
|
||||
@staticmethod
|
||||
def arrival_notice(title: str) -> tuple[str, str]:
|
||||
notification_title = 'Поступление по закупке'
|
||||
description = (
|
||||
f'По заявке "{title}" есть поступление. '
|
||||
f'Проверьте позиции и подтвердите получение.'
|
||||
)
|
||||
return notification_title, description
|
||||
|
||||
@staticmethod
|
||||
def cancelled(title: str, reason: str = 'не указана') -> tuple[str, str]:
|
||||
"""
|
||||
|
||||
@ -390,34 +390,36 @@ def notify_request_completed(request):
|
||||
)
|
||||
|
||||
|
||||
def notify_request_arrival(request, actor=None):
|
||||
"""Вручную уведомить заказчика о поступлении по заявке."""
|
||||
notification_title, description = MessageTemplates.arrival_notice(
|
||||
request.title,
|
||||
)
|
||||
notify_requestor(
|
||||
request,
|
||||
NotificationVerbs.ARRIVAL_NOTICE,
|
||||
notification_title,
|
||||
description,
|
||||
actor=actor,
|
||||
extra_data={
|
||||
"status": request.status,
|
||||
"fulfillment_status": request.fulfillment_status,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def notify_request_in_progress(request, executor):
|
||||
"""
|
||||
Уведомить о взятии заявки в работу.
|
||||
|
||||
Отправляет уведомления:
|
||||
- Создателю заявки (если он не сам взял в работу)
|
||||
Заказчика не уведомляем: это промежуточный статус обработки.
|
||||
Финальные статусы и комментарии остаются основными сигналами.
|
||||
|
||||
Args:
|
||||
request: Объект ProcurementRequest
|
||||
executor: Пользователь, взявший заявку в работу
|
||||
"""
|
||||
executor_name = executor.get_full_name() if executor else 'Сотрудник'
|
||||
|
||||
notification_title, description = MessageTemplates.in_progress_requestor(
|
||||
request.title, executor_name
|
||||
)
|
||||
notify_requestor(
|
||||
request,
|
||||
NotificationVerbs.IN_PROGRESS,
|
||||
notification_title,
|
||||
description,
|
||||
actor=executor,
|
||||
extra_data={
|
||||
"old_status": getattr(request, "_original_status", None),
|
||||
"new_status": request.status,
|
||||
"executor_id": executor.id if executor else None,
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
def notify_executor_reassigned(request, previous_executor, actor):
|
||||
@ -488,22 +490,8 @@ 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,
|
||||
actor=approval.approver,
|
||||
extra_data={
|
||||
"approval_id": approval.id,
|
||||
"approval_priority": approval.priority,
|
||||
},
|
||||
)
|
||||
# Не уведомляем заказчика о каждом промежуточном этапе согласования.
|
||||
# Следующий согласующий всё ещё должен получить pending-уведомление.
|
||||
notify_approvers(approval.request)
|
||||
|
||||
|
||||
@ -534,26 +522,8 @@ def notify_stage_rejected(approval):
|
||||
|
||||
|
||||
def notify_item_updated(item, actor=None, changed_fields=None):
|
||||
"""Уведомить автора заявки об изменении позиции."""
|
||||
procurement_request = item.request
|
||||
actor_name = _actor_name(actor)
|
||||
notification_title, description = MessageTemplates.item_updated(
|
||||
procurement_request.title,
|
||||
item.name,
|
||||
actor_name,
|
||||
)
|
||||
notify_requestor(
|
||||
procurement_request,
|
||||
NotificationVerbs.ITEM_UPDATED,
|
||||
notification_title,
|
||||
description,
|
||||
actor=actor,
|
||||
extra_data={
|
||||
"item_id": item.id,
|
||||
"item_name": item.name,
|
||||
"changed_fields": changed_fields or [],
|
||||
},
|
||||
)
|
||||
"""Не уведомлять заказчика о технических изменениях позиции."""
|
||||
return
|
||||
|
||||
|
||||
def notify_item_issue_reported(item, actor=None):
|
||||
|
||||
@ -1915,7 +1915,7 @@ class TestProcessingDepartmentWorkflow:
|
||||
assert completed_notifications[0]["sender"] == supply_user
|
||||
assert completed_notifications[0]["data"]["actor_id"] == supply_user.id
|
||||
|
||||
def test_item_update_notifies_requestor(
|
||||
def test_item_update_does_not_notify_requestor(
|
||||
self, api_client, supply_user, processing_request,
|
||||
procurement_item_factory, monkeypatch
|
||||
):
|
||||
@ -1959,19 +1959,10 @@ class TestProcessingDepartmentWorkflow:
|
||||
item for item in sent
|
||||
if item["verb"] == "procurement_item_updated"
|
||||
]
|
||||
assert len(item_notifications) == 1
|
||||
assert item_notifications[0]["recipient"] == processing_request.requestor
|
||||
assert item_notifications[0]["data"]["item_id"] == item.id
|
||||
assert sorted(item_notifications[0]["data"]["changed_fields"]) == [
|
||||
"actual_unit_price",
|
||||
"execution_status",
|
||||
"expected_delivery_dates",
|
||||
"links",
|
||||
"ordered_quantity",
|
||||
]
|
||||
assert item_notifications == []
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_item_order_update_routes_push_to_requestor(
|
||||
def test_item_order_update_does_not_route_push_to_requestor(
|
||||
self, api_client, supply_user, processing_request,
|
||||
procurement_item_factory
|
||||
):
|
||||
@ -2016,16 +2007,12 @@ class TestProcessingDepartmentWorkflow:
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
notification = Notification.objects.get(
|
||||
assert not Notification.objects.filter(
|
||||
recipient=processing_request.requestor,
|
||||
verb="procurement_item_updated",
|
||||
data__item_id=item.id,
|
||||
)
|
||||
assert notification.data["changed_fields"] == [
|
||||
"ordered_quantity",
|
||||
"execution_status",
|
||||
]
|
||||
send_push.assert_called_once_with(notification.id)
|
||||
).exists()
|
||||
send_push.assert_not_called()
|
||||
|
||||
def test_item_expected_delivery_dates_must_be_valid_dates(
|
||||
self, api_client, supply_user, processing_request,
|
||||
@ -2381,7 +2368,7 @@ class TestProcessingDepartmentWorkflow:
|
||||
]
|
||||
assert supply_user.email in issue_recipients
|
||||
|
||||
def test_executor_issue_reopens_request_for_requestor_and_notifies(
|
||||
def test_executor_issue_reopens_request_for_requestor_without_status_spam(
|
||||
self, api_client, user, supply_user, processing_request,
|
||||
procurement_item_factory, monkeypatch
|
||||
):
|
||||
@ -2445,8 +2432,8 @@ class TestProcessingDepartmentWorkflow:
|
||||
for notification in sent
|
||||
if notification["verb"] == "procurement_in_progress"
|
||||
]
|
||||
assert user.email in item_updated_recipients
|
||||
assert user.email in in_progress_recipients
|
||||
assert user.email not in item_updated_recipients
|
||||
assert user.email not in in_progress_recipients
|
||||
|
||||
def test_requestor_can_confirm_item_received(
|
||||
self, api_client, user, supply_user, processing_request,
|
||||
@ -3079,10 +3066,7 @@ class TestProcessingDepartmentWorkflow:
|
||||
for item in sent
|
||||
if item["verb"] == "procurement_item_updated"
|
||||
]
|
||||
assert item_update_recipients == [
|
||||
processing_request.requestor.email,
|
||||
processing_request.requestor.email,
|
||||
]
|
||||
assert item_update_recipients == []
|
||||
completed_recipients = [
|
||||
item["recipient"].email
|
||||
for item in sent
|
||||
@ -3090,6 +3074,109 @@ class TestProcessingDepartmentWorkflow:
|
||||
]
|
||||
assert completed_recipients == [processing_request.requestor.email]
|
||||
|
||||
def test_processing_user_can_notify_requestor_about_arrival_multiple_times(
|
||||
self, api_client, user, supply_user, processing_request, monkeypatch
|
||||
):
|
||||
sent = []
|
||||
|
||||
def fake_notify_send(**kwargs):
|
||||
sent.append(kwargs)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"procurement.notifications.handlers.notify.send",
|
||||
fake_notify_send,
|
||||
)
|
||||
|
||||
processing_request.executor = supply_user
|
||||
processing_request.status = ProcurementStatus.IN_PROGRESS
|
||||
processing_request.save()
|
||||
sent.clear()
|
||||
|
||||
api_client.force_authenticate(user=supply_user)
|
||||
url = reverse(
|
||||
'api:v1:procurement:procurementrequest-notify-arrival',
|
||||
kwargs={'pk': processing_request.id},
|
||||
)
|
||||
|
||||
first_response = api_client.post(url)
|
||||
second_response = api_client.post(url)
|
||||
|
||||
assert first_response.status_code == status.HTTP_200_OK
|
||||
assert second_response.status_code == status.HTTP_200_OK
|
||||
arrival_notifications = [
|
||||
item for item in sent
|
||||
if item["verb"] == "procurement_arrival_notice"
|
||||
]
|
||||
assert len(arrival_notifications) == 2
|
||||
assert [
|
||||
item["recipient"].email for item in arrival_notifications
|
||||
] == [user.email, user.email]
|
||||
assert all(item["sender"] == supply_user for item in arrival_notifications)
|
||||
assert all(
|
||||
item["data"]["request_id"] == processing_request.id
|
||||
for item in arrival_notifications
|
||||
)
|
||||
|
||||
def test_notify_arrival_is_not_available_after_request_completed(
|
||||
self, api_client, supply_user, processing_request, monkeypatch
|
||||
):
|
||||
sent = []
|
||||
|
||||
def fake_notify_send(**kwargs):
|
||||
sent.append(kwargs)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"procurement.notifications.handlers.notify.send",
|
||||
fake_notify_send,
|
||||
)
|
||||
|
||||
processing_request.executor = supply_user
|
||||
processing_request.status = ProcurementStatus.COMPLETED
|
||||
processing_request.completed_at = timezone.now()
|
||||
processing_request.save()
|
||||
sent.clear()
|
||||
|
||||
api_client.force_authenticate(user=supply_user)
|
||||
url = reverse(
|
||||
'api:v1:procurement:procurementrequest-notify-arrival',
|
||||
kwargs={'pk': processing_request.id},
|
||||
)
|
||||
|
||||
response = api_client.post(url)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert [
|
||||
item for item in sent
|
||||
if item["verb"] == "procurement_arrival_notice"
|
||||
] == []
|
||||
|
||||
def test_requestor_cannot_notify_self_about_arrival(
|
||||
self, api_client, user, processing_request, monkeypatch
|
||||
):
|
||||
sent = []
|
||||
|
||||
def fake_notify_send(**kwargs):
|
||||
sent.append(kwargs)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"procurement.notifications.handlers.notify.send",
|
||||
fake_notify_send,
|
||||
)
|
||||
|
||||
api_client.force_authenticate(user=user)
|
||||
url = reverse(
|
||||
'api:v1:procurement:procurementrequest-notify-arrival',
|
||||
kwargs={'pk': processing_request.id},
|
||||
)
|
||||
|
||||
response = api_client.post(url)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert [
|
||||
item for item in sent
|
||||
if item["verb"] == "procurement_arrival_notice"
|
||||
] == []
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# ТЕСТЫ WORKFLOW ДЕЙСТВИЙ
|
||||
@ -3217,12 +3304,11 @@ class TestProcurementRequestWorkflow:
|
||||
if item["verb"] == "procurement_pending_approval"
|
||||
]
|
||||
assert pending_recipients == ["user@example.com"]
|
||||
stage_recipients = [
|
||||
item["recipient"].email
|
||||
for item in sent
|
||||
stage_notifications = [
|
||||
item for item in sent
|
||||
if item["verb"] == "procurement_stage_approved"
|
||||
]
|
||||
assert stage_recipients == [procurement_request.requestor.email]
|
||||
assert stage_notifications == []
|
||||
|
||||
def test_final_approve_does_not_notify_future_approvers(
|
||||
self, api_client, approver_with_permission, procurement_request, procurement_item, monkeypatch
|
||||
@ -3368,10 +3454,10 @@ class TestProcurementRequestWorkflow:
|
||||
]
|
||||
assert cancelled_recipients == ["head@example.com"]
|
||||
|
||||
def test_start_work_notifies_requestor(
|
||||
def test_start_work_does_not_notify_requestor(
|
||||
self, api_client, user, staff_user, department_head, procurement_request, procurement_item, monkeypatch
|
||||
):
|
||||
"""При взятии в работу уведомляется автор заявки."""
|
||||
"""При взятии в работу автор заявки не получает шумное уведомление."""
|
||||
sent = []
|
||||
|
||||
def fake_notify_send(**kwargs):
|
||||
@ -3416,9 +3502,7 @@ class TestProcurementRequestWorkflow:
|
||||
for item in sent
|
||||
if item["verb"] == "procurement_in_progress"
|
||||
]
|
||||
assert in_progress_recipients == [
|
||||
procurement_request.requestor.email,
|
||||
]
|
||||
assert procurement_request.requestor.email not in in_progress_recipients
|
||||
|
||||
def test_complete_notifies_requestor(
|
||||
self, api_client, user, staff_user, department_head, procurement_request, procurement_item, monkeypatch
|
||||
|
||||
@ -449,6 +449,17 @@ html[data-bs-theme="dark"] .app-input::-webkit-calendar-picker-indicator {
|
||||
border-color: color-mix(in srgb, #f97316 40%, var(--border-strong));
|
||||
}
|
||||
|
||||
.app-action-notify {
|
||||
border: 1px solid color-mix(in srgb, #8b5cf6 28%, var(--border-strong));
|
||||
background: color-mix(in srgb, #8b5cf6 16%, transparent);
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.app-action-notify:hover {
|
||||
background: color-mix(in srgb, #8b5cf6 22%, transparent);
|
||||
border-color: color-mix(in srgb, #8b5cf6 40%, var(--border-strong));
|
||||
}
|
||||
|
||||
.app-action-secondary {
|
||||
border: 1px solid var(--border-strong);
|
||||
background: var(--surface-elevated);
|
||||
|
||||
@ -166,6 +166,7 @@ function ProcurementRequestActionButtons({
|
||||
onReject,
|
||||
onStart,
|
||||
onComplete,
|
||||
onNotifyArrival,
|
||||
onCancel,
|
||||
}: {
|
||||
request: ProcurementRequest;
|
||||
@ -184,6 +185,7 @@ function ProcurementRequestActionButtons({
|
||||
onReject: (id: number) => void | Promise<unknown>;
|
||||
onStart: (id: number) => void | Promise<unknown>;
|
||||
onComplete: (id: number) => void | Promise<unknown>;
|
||||
onNotifyArrival: (id: number) => void | Promise<unknown>;
|
||||
onCancel: (id: number) => void | Promise<unknown>;
|
||||
}) {
|
||||
const status = String(request.status || "").toLowerCase();
|
||||
@ -191,6 +193,10 @@ function ProcurementRequestActionButtons({
|
||||
const isPending = status === "pending";
|
||||
const canStartWork = Boolean(request.can_current_user_start_work);
|
||||
const isInProgress = status === "in_progress";
|
||||
const canNotifyArrival = Boolean(
|
||||
request.can_current_user_process_items &&
|
||||
(status === "waiting" || status === "in_progress")
|
||||
);
|
||||
const startWorkLabel = isInProgress ? "Забрать в работу" : "Взять в работу";
|
||||
const canApproveThis = Boolean(request.can_current_user_approve);
|
||||
const canSubmitForApproval = Boolean(request.can_current_user_submit_for_approval);
|
||||
@ -286,6 +292,18 @@ function ProcurementRequestActionButtons({
|
||||
{label("Закрыть заявку")}
|
||||
</button>
|
||||
) : null}
|
||||
{canNotifyArrival ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onNotifyArrival(request.id)}
|
||||
disabled={busyKey === `notify-arrival-${request.id}`}
|
||||
title="Уведомить о поступлении"
|
||||
className={buttonClass("app-action-notify")}
|
||||
>
|
||||
<Package size={14} />
|
||||
{label("Уведомить о поступлении")}
|
||||
</button>
|
||||
) : null}
|
||||
{showSecondaryActions && isAuthor && !isFinal(status) && status !== "draft" ? (
|
||||
<button
|
||||
type="button"
|
||||
@ -312,7 +330,6 @@ export default function ProcurementPage() {
|
||||
activeFilterCount,
|
||||
activeSection,
|
||||
actionError,
|
||||
actionSuccess,
|
||||
addItemRow,
|
||||
busyKey,
|
||||
canManage,
|
||||
@ -344,6 +361,7 @@ export default function ProcurementPage() {
|
||||
handleCancel,
|
||||
handleComplete,
|
||||
handleMarkAllReceived,
|
||||
handleNotifyArrival,
|
||||
handleToggleViewed,
|
||||
handleReportItemIssue,
|
||||
handleCancelItemIssue,
|
||||
@ -635,7 +653,6 @@ export default function ProcurementPage() {
|
||||
|
||||
{/* ── alerts ── */}
|
||||
{actionError && <p className="mb-3 rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700">{actionError}</p>}
|
||||
{actionSuccess && <p className="app-feedback-success mb-3 rounded-lg px-3 py-2 text-sm">{actionSuccess}</p>}
|
||||
|
||||
{activeSection === "stats" && <ProcurementStatsPanel />}
|
||||
|
||||
@ -1072,6 +1089,7 @@ export default function ProcurementPage() {
|
||||
onReject={(id) => openRequestActionDialog("reject", id)}
|
||||
onStart={handleStart}
|
||||
onComplete={handleComplete}
|
||||
onNotifyArrival={handleNotifyArrival}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</div>
|
||||
@ -1159,6 +1177,7 @@ export default function ProcurementPage() {
|
||||
onReject={handleReject}
|
||||
onStart={handleStart}
|
||||
onComplete={handleComplete}
|
||||
onNotifyArrival={handleNotifyArrival}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
@ -1207,6 +1226,7 @@ export default function ProcurementPage() {
|
||||
onReject={(id) => openRequestActionDialog("reject", id)}
|
||||
onStart={handleStart}
|
||||
onComplete={handleComplete}
|
||||
onNotifyArrival={handleNotifyArrival}
|
||||
onCancel={(id) => openRequestActionDialog("cancel", id)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -5,6 +5,7 @@ import { apiClient } from "@/lib/api";
|
||||
import { canManageRequests, canManageSupplier } from "@/lib/permissions";
|
||||
import { cleanLinkRows, toLinkRows, validateLinkRows } from "@/lib/procurementLinks";
|
||||
import { displayUserName, extractNextPage, loadAllPages } from "@/lib/shared";
|
||||
import { toast } from "sonner";
|
||||
import type {
|
||||
Department,
|
||||
ProcurementComment,
|
||||
@ -142,7 +143,6 @@ export function useProcurementPage(user: User | null) {
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
const [actionSuccess, setActionSuccess] = useState<string | null>(null);
|
||||
const [busyKey, setBusyKey] = useState<string | null>(null);
|
||||
const [nextPage, setNextPage] = useState<number | null>(null);
|
||||
const [scopeCounts, setScopeCounts] = useState<Record<ScopeTab, number>>({
|
||||
@ -412,14 +412,12 @@ export function useProcurementPage(user: User | null) {
|
||||
setEditingId(null);
|
||||
resetForm();
|
||||
setActionError(null);
|
||||
setActionSuccess(null);
|
||||
setCreateOpen(true);
|
||||
}, [resetForm]);
|
||||
|
||||
const openEdit = useCallback(async (request: ProcurementRequest) => {
|
||||
setCreateOpen(false);
|
||||
setActionError(null);
|
||||
setActionSuccess(null);
|
||||
|
||||
let detail: ProcurementRequest;
|
||||
try {
|
||||
@ -614,7 +612,7 @@ export function useProcurementPage(user: User | null) {
|
||||
|
||||
if (modalMode === "create") {
|
||||
await apiClient.createProcurementRequest(payload) as ProcurementRequest;
|
||||
setActionSuccess("Заявка создана и направлена в отдел.");
|
||||
toast.success("Заявка создана и направлена в отдел.");
|
||||
setCreateOpen(false);
|
||||
} else if (editingId) {
|
||||
await apiClient.updateProcurementRequest(editingId, {
|
||||
@ -624,7 +622,7 @@ export function useProcurementPage(user: User | null) {
|
||||
processing_department: payload.processing_department,
|
||||
items: payload.items,
|
||||
});
|
||||
setActionSuccess("Заявка обновлена.");
|
||||
toast.success("Заявка обновлена.");
|
||||
setEditingId(null);
|
||||
}
|
||||
|
||||
@ -644,7 +642,7 @@ export function useProcurementPage(user: User | null) {
|
||||
setBusyKey(key);
|
||||
setActionError(null);
|
||||
await action();
|
||||
setActionSuccess(successMessage);
|
||||
toast.success(successMessage);
|
||||
await refreshAfterMutation(id);
|
||||
return true;
|
||||
} catch (actionErrorValue) {
|
||||
@ -915,6 +913,13 @@ export function useProcurementPage(user: User | null) {
|
||||
"Все позиции отмечены полученными.",
|
||||
), [doAction]);
|
||||
|
||||
const handleNotifyArrival = useCallback((id: number) => doAction(
|
||||
`notify-arrival-${id}`,
|
||||
() => apiClient.notifyProcurementRequestArrival(id),
|
||||
id,
|
||||
"Заказчик уведомлён о поступлении.",
|
||||
), [doAction]);
|
||||
|
||||
const handleToggleViewed = useCallback(async (id: number, isViewed: boolean) => {
|
||||
try {
|
||||
setBusyKey(`viewed-${id}`);
|
||||
@ -958,7 +963,7 @@ export function useProcurementPage(user: User | null) {
|
||||
}
|
||||
}
|
||||
await apiClient.updateProcurementItem(itemId, patch);
|
||||
setActionSuccess("Позиция обновлена.");
|
||||
toast.success("Позиция обновлена.");
|
||||
await refreshAfterMutation(requestId);
|
||||
return true;
|
||||
} catch (updateError) {
|
||||
@ -974,7 +979,7 @@ export function useProcurementPage(user: User | null) {
|
||||
setBusyKey(`item-issue-${itemId}`);
|
||||
setActionError(null);
|
||||
await apiClient.reportProcurementItemIssue(itemId, text);
|
||||
setActionSuccess("Позиция отмечена как проблемная.");
|
||||
toast.success("Позиция отмечена как проблемная.");
|
||||
await refreshAfterMutation(requestId);
|
||||
return true;
|
||||
} catch (issueError) {
|
||||
@ -990,7 +995,7 @@ export function useProcurementPage(user: User | null) {
|
||||
setBusyKey(`item-cancel-issue-${itemId}`);
|
||||
setActionError(null);
|
||||
await apiClient.cancelProcurementItemIssue(itemId);
|
||||
setActionSuccess("Отметка брака снята.");
|
||||
toast.success("Отметка брака снята.");
|
||||
await refreshAfterMutation(requestId);
|
||||
return true;
|
||||
} catch (issueError) {
|
||||
@ -1006,7 +1011,7 @@ export function useProcurementPage(user: User | null) {
|
||||
setBusyKey(`item-confirm-received-${itemId}`);
|
||||
setActionError(null);
|
||||
await apiClient.confirmProcurementItemReceived(itemId, receivedQuantity);
|
||||
setActionSuccess("Получение позиции подтверждено.");
|
||||
toast.success("Получение позиции подтверждено.");
|
||||
await refreshAfterMutation(requestId);
|
||||
return true;
|
||||
} catch (confirmError) {
|
||||
@ -1022,7 +1027,7 @@ export function useProcurementPage(user: User | null) {
|
||||
setBusyKey(`item-cancel-received-${itemId}`);
|
||||
setActionError(null);
|
||||
await apiClient.cancelProcurementItemReceived(itemId, cancelQuantity);
|
||||
setActionSuccess("Получение позиции отменено.");
|
||||
toast.success("Получение позиции отменено.");
|
||||
await refreshAfterMutation(requestId);
|
||||
return true;
|
||||
} catch (cancelError) {
|
||||
@ -1049,7 +1054,7 @@ export function useProcurementPage(user: User | null) {
|
||||
detailsCacheRef.current = next;
|
||||
return next;
|
||||
});
|
||||
setActionSuccess("Заявка удалена.");
|
||||
toast.success("Заявка удалена.");
|
||||
await refreshCurrentView();
|
||||
return true;
|
||||
} catch (deleteError) {
|
||||
@ -1093,7 +1098,6 @@ export function useProcurementPage(user: User | null) {
|
||||
loadingMore,
|
||||
error,
|
||||
actionError,
|
||||
actionSuccess,
|
||||
busyKey,
|
||||
nextPage,
|
||||
scopeCounts,
|
||||
@ -1148,6 +1152,7 @@ export function useProcurementPage(user: User | null) {
|
||||
handleStart,
|
||||
handleComplete,
|
||||
handleMarkAllReceived,
|
||||
handleNotifyArrival,
|
||||
handleToggleViewed,
|
||||
handleUpdateItem,
|
||||
handleReportItemIssue,
|
||||
|
||||
@ -16,6 +16,7 @@ export function createProcurementApi(request: RequestFn) {
|
||||
startWorkProcurementRequest: (id: number) => request(`/api/v1/procurement/requests/${id}/start_work/`, { method: 'POST' }),
|
||||
completeProcurementRequest: (id: number) => request(`/api/v1/procurement/requests/${id}/complete/`, { method: 'POST' }),
|
||||
markAllReceivedProcurementRequest: (id: number) => request(`/api/v1/procurement/requests/${id}/mark_all_received/`, { method: 'POST' }),
|
||||
notifyProcurementRequestArrival: (id: number) => request(`/api/v1/procurement/requests/${id}/notify_arrival/`, { method: 'POST' }),
|
||||
setProcurementRequestViewed: (id: number, isViewed: boolean) =>
|
||||
request(`/api/v1/procurement/requests/${id}/set_viewed/`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_viewed: isViewed }) }),
|
||||
cancelProcurementRequest: (id: number, reason?: string) =>
|
||||
|
||||
@ -45,11 +45,14 @@ export const VERB_CATEGORIES: Record<string, string> = {
|
||||
procurement_rejected: 'Закупки',
|
||||
procurement_in_progress: 'Закупки',
|
||||
procurement_executor_reassigned: 'Закупки',
|
||||
procurement_arrival_notice: 'Закупки',
|
||||
procurement_completed: 'Закупки',
|
||||
procurement_cancelled: 'Закупки',
|
||||
procurement_item_updated: 'Закупки',
|
||||
procurement_request_commented: 'Закупки',
|
||||
procurement_item_commented: 'Закупки',
|
||||
equipment_transferred: 'Закупки',
|
||||
equipment_maintenance: 'Закупки',
|
||||
|
||||
// Новости
|
||||
feed_new_post: 'Новости',
|
||||
@ -101,11 +104,14 @@ export const VERB_NAMES: Record<string, string> = {
|
||||
procurement_rejected: 'Закупка отклонена',
|
||||
procurement_in_progress: 'Закупка взята в работу',
|
||||
procurement_executor_reassigned: 'Заявку забрал другой сотрудник',
|
||||
procurement_arrival_notice: 'Поступление по закупке',
|
||||
procurement_completed: 'Закупка завершена',
|
||||
procurement_cancelled: 'Закупка отменена',
|
||||
procurement_item_updated: 'Позиция закупки изменена',
|
||||
procurement_request_commented: 'Комментарий к закупке',
|
||||
procurement_item_commented: 'Комментарий к позиции закупки',
|
||||
equipment_transferred: 'Оборудование передано',
|
||||
equipment_maintenance: 'Обслуживание оборудования',
|
||||
|
||||
// Новости
|
||||
feed_new_post: 'Новая публикация',
|
||||
@ -121,7 +127,9 @@ export const VERB_NAMES: Record<string, string> = {
|
||||
* Получить категорию для verb
|
||||
*/
|
||||
export function getVerbCategory(verb: string): string {
|
||||
return VERB_CATEGORIES[verb] || 'Общее';
|
||||
if (VERB_CATEGORIES[verb]) return VERB_CATEGORIES[verb];
|
||||
if (verb.startsWith('procurement_') || verb.startsWith('equipment_')) return 'Закупки';
|
||||
return 'Общее';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user