procurement: route requests through processing department

This commit is contained in:
lizerk 2026-05-26 17:25:31 +03:00
parent da986c0e29
commit 4a05618ed7
11 changed files with 412 additions and 90 deletions

View File

@ -348,6 +348,7 @@ class ProcurementRequestListSerializer(
)
comments_count = serializers.IntegerField(read_only=True, default=0)
can_current_user_approve = serializers.SerializerMethodField()
can_current_user_submit_for_approval = serializers.SerializerMethodField()
@extend_schema_field(serializers.BooleanField())
def get_can_current_user_approve(self, obj):
@ -355,6 +356,15 @@ class ProcurementRequestListSerializer(
user = getattr(request, "user", None)
return ProcurementApprovalResolver.user_can_approve(user, obj)
@extend_schema_field(serializers.BooleanField())
def get_can_current_user_submit_for_approval(self, obj):
request = self.context.get("request")
user = getattr(request, "user", None)
return ProcurementApprovalResolver.user_can_submit_for_approval(
user,
obj,
)
class Meta:
model = ProcurementRequest
fields = [
@ -386,6 +396,7 @@ class ProcurementRequestListSerializer(
"total_received_quantity",
"comments_count",
"can_current_user_approve",
"can_current_user_submit_for_approval",
"created_at",
"submitted_at",
"started_at",
@ -415,6 +426,7 @@ class ProcurementRequestListSerializer(
"total_received_quantity",
"comments_count",
"can_current_user_approve",
"can_current_user_submit_for_approval",
]
def update(self, instance, validated_data):
@ -470,6 +482,7 @@ class ProcurementRequestDetailSerializer(
is_editable = serializers.BooleanField(read_only=True)
comments_count = serializers.IntegerField(read_only=True, default=0)
can_current_user_approve = serializers.SerializerMethodField()
can_current_user_submit_for_approval = serializers.SerializerMethodField()
@extend_schema_field(serializers.BooleanField())
def get_can_current_user_approve(self, obj):
@ -477,6 +490,15 @@ class ProcurementRequestDetailSerializer(
user = getattr(request, "user", None)
return ProcurementApprovalResolver.user_can_approve(user, obj)
@extend_schema_field(serializers.BooleanField())
def get_can_current_user_submit_for_approval(self, obj):
request = self.context.get("request")
user = getattr(request, "user", None)
return ProcurementApprovalResolver.user_can_submit_for_approval(
user,
obj,
)
class Meta:
model = ProcurementRequest
fields = [
@ -514,6 +536,7 @@ class ProcurementRequestDetailSerializer(
"is_editable",
"comments_count",
"can_current_user_approve",
"can_current_user_submit_for_approval",
"created_at",
"updated_at",
"submitted_at",
@ -543,6 +566,7 @@ class ProcurementRequestDetailSerializer(
"fulfillment_status_display",
"comments_count",
"can_current_user_approve",
"can_current_user_submit_for_approval",
]

View File

@ -105,6 +105,8 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
"""Выбрать права доступа в зависимости от действия."""
if self.action in ["approve", "reject"]:
permission_classes = [CanApproveProcurementRequest]
elif self.action == "submit":
permission_classes = [permissions.IsAuthenticated]
else:
# Для create, update, delete и остальных действий
permission_classes = [CanManageProcurementRequest]
@ -331,25 +333,30 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
procurement_request = self.get_object()
if procurement_request.processing_department_id:
if (
procurement_request.status == ProcurementStatus.PENDING
or procurement_request.approvals.filter(
status=ApprovalStatus.PENDING,
).exists()
):
return Response(
{
"error": (
"Заявка уже направлена в отдел-исполнитель "
"и не требует согласования"
)
},
{"error": "Заявка уже находится на согласовании"},
status=status.HTTP_400_BAD_REQUEST,
)
# Проверки
if procurement_request.requestor != request.user:
if not ProcurementApprovalResolver.user_can_submit_for_approval(
request.user,
procurement_request,
):
return Response(
{"error": "Вы не можете отправить чужую заявку"},
{"error": "Вы не можете отправить эту заявку на согласование"},
status=status.HTTP_403_FORBIDDEN,
)
if not procurement_request.is_editable:
if (
not procurement_request.processing_department_id
and not procurement_request.is_editable
):
return Response(
{"error": "Заявка уже отправлена на согласование"},
status=status.HTTP_400_BAD_REQUEST,
@ -479,12 +486,16 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet):
).count()
if pending_approvals == 0:
# Все одобрили - меняем статус заявки
# Сигнал post_save(ProcurementRequest) отправит уведомление
# requestor'у
procurement_request.status = ProcurementStatus.APPROVED
if procurement_request.processing_department_id:
procurement_request.status = (
ProcurementStatus.IN_PROGRESS
if procurement_request.executor_id
else ProcurementStatus.WAITING
)
else:
procurement_request.status = ProcurementStatus.APPROVED
procurement_request._notification_actor = request.user
procurement_request.save()
procurement_request.save(update_fields=["status", "updated_at"])
procurement_request.refresh_from_db()
@ -930,6 +941,7 @@ class ProcurementItemViewSet(viewsets.ModelViewSet):
user = self.request.user
if procurement_request.status in [
ProcurementStatus.PENDING,
ProcurementStatus.CANCELLED,
ProcurementStatus.REJECTED,
]:
@ -970,6 +982,7 @@ class ProcurementItemViewSet(viewsets.ModelViewSet):
procurement_request = item.request
if procurement_request.status in [
ProcurementStatus.PENDING,
ProcurementStatus.CANCELLED,
ProcurementStatus.REJECTED,
]:
@ -1040,11 +1053,17 @@ class ProcurementItemViewSet(viewsets.ModelViewSet):
)
if procurement_request.status in [
ProcurementStatus.PENDING,
ProcurementStatus.CANCELLED,
ProcurementStatus.REJECTED,
]:
return Response(
{"detail": "Нельзя изменить позицию в отменённой или отклонённой заявке"},
{
"detail": (
"Нельзя изменить позицию в заявке на согласовании, "
"в отменённой или отклонённой заявке"
)
},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@ -36,6 +36,7 @@ from .handlers import (
notify_request_rejected,
notify_request_completed,
notify_request_in_progress,
notify_request_returned_to_processing,
notify_request_cancelled,
notify_stage_approved,
notify_stage_rejected,
@ -65,6 +66,7 @@ __all__ = [
'notify_request_rejected',
'notify_request_completed',
'notify_request_in_progress',
'notify_request_returned_to_processing',
'notify_request_cancelled',
'notify_stage_approved',
'notify_stage_rejected',

View File

@ -312,6 +312,39 @@ def notify_request_approved(request):
)
def notify_request_returned_to_processing(request):
"""Уведомить исполнителя или отдел, что согласованная заявка вернулась."""
if not request.processing_department_id:
return
actor = _request_actor(request)
title = "Заявка вернулась в обработку"
description = (
f'Заявка "{request.title}" согласована и снова доступна '
f'для обработки.'
)
recipients = []
if request.executor_id:
recipients.append(request.executor)
else:
recipients.extend(_processing_department_recipients(request))
_notify_many(
recipients,
verb=NotificationVerbs.APPROVED,
procurement_request=request,
title=title,
description=description,
actor=actor,
extra_data={
"old_status": getattr(request, "_original_status", None),
"new_status": request.status,
"processing_department_id": request.processing_department_id,
"executor_id": request.executor_id,
},
)
def notify_request_rejected(request):
"""
Уведомить об отклонении заявки.

View File

@ -23,6 +23,7 @@ from .handlers import (
notify_request_rejected,
notify_request_completed,
notify_request_in_progress,
notify_request_returned_to_processing,
notify_request_cancelled,
notify_stage_approved,
notify_stage_rejected,
@ -85,6 +86,18 @@ def _handle_status_change(request, new_status):
# Заявка отправлена на согласование
notify_approvers(request)
elif (
getattr(request, "_original_status", None) == ProcurementStatus.PENDING
and request.processing_department_id
and new_status in {
ProcurementStatus.WAITING,
ProcurementStatus.IN_PROGRESS,
}
):
# Адресная заявка после согласования возвращается в тот же отдел.
notify_request_approved(request)
notify_request_returned_to_processing(request)
elif new_status == ProcurementStatus.APPROVED:
# Заявка одобрена
notify_request_approved(request)

View File

@ -9,6 +9,7 @@ from django.utils import timezone
from procurement.constants import (
ApprovalStatus,
ProcurementStatus,
get_default_approval_step_name,
)
@ -93,6 +94,60 @@ class ProcurementApprovalResolver:
через Department.head.
"""
@classmethod
def user_is_processing_department_member(cls, user, procurement_request):
if not procurement_request.processing_department_id:
return False
from employees.models import EmployeeDepartment
return (
EmployeeDepartment.objects.filter(
employee_id=user.id,
department_id=procurement_request.processing_department_id,
is_active=True,
).exists()
or procurement_request.processing_department.head_id == user.id
)
@classmethod
def user_can_submit_for_approval(cls, user, procurement_request) -> bool:
if not user or not user.is_authenticated:
return False
if (
procurement_request.status == ProcurementStatus.PENDING
or procurement_request.approvals.filter(
status=ApprovalStatus.PENDING,
).exists()
):
return False
if procurement_request.processing_department_id:
if procurement_request.status not in {
ProcurementStatus.WAITING,
ProcurementStatus.IN_PROGRESS,
}:
return False
if (
user.is_superuser
or user.is_staff
or user.has_perm("procurement.change_procurementrequest")
or user.has_perm("procurement.execute_procurement")
):
return True
if procurement_request.executor_id == user.id:
return True
return cls.user_is_processing_department_member(
user,
procurement_request,
)
return (
procurement_request.requestor_id == user.id
and procurement_request.is_editable
)
@classmethod
def resolve_approval_step(cls, procurement_request, route):
from .models import ApprovalRoute

View File

@ -982,6 +982,221 @@ class TestProcessingDepartmentWorkflow:
)
assert edit_response.status_code == status.HTTP_403_FORBIDDEN
@pytest.fixture
def department_head_approval_route(self, department_head):
return ApprovalRoute.objects.create(
priority=HEAD_PRIORITY,
resolver_type=ApprovalRoute.ResolverType.DEPARTMENT_HEAD,
)
def test_processing_department_user_can_submit_waiting_request_for_approval(
self, api_client, supply_user, processing_request,
procurement_item_factory, department_head_approval_route
):
procurement_item_factory(request=processing_request)
api_client.force_authenticate(user=supply_user)
url = reverse(
'api:v1:procurement:procurementrequest-submit',
kwargs={'pk': processing_request.id},
)
response = api_client.post(url)
assert response.status_code == status.HTTP_200_OK
processing_request.refresh_from_db()
assert processing_request.status == ProcurementStatus.PENDING
assert processing_request.processing_department_id is not None
assert processing_request.approvals.count() == 1
assert response.data["can_current_user_submit_for_approval"] is False
def test_processing_department_requestor_cannot_submit_for_approval(
self, api_client, user, processing_request,
procurement_item_factory, department_head_approval_route
):
procurement_item_factory(request=processing_request)
api_client.force_authenticate(user=user)
url = reverse(
'api:v1:procurement:procurementrequest-submit',
kwargs={'pk': processing_request.id},
)
response = api_client.post(url)
assert response.status_code == status.HTTP_403_FORBIDDEN
processing_request.refresh_from_db()
assert processing_request.status == ProcurementStatus.WAITING
assert processing_request.approvals.count() == 0
def test_processing_department_request_cannot_be_submitted_twice_while_pending(
self, api_client, supply_user, processing_request,
procurement_item_factory, department_head_approval_route
):
procurement_item_factory(request=processing_request)
processing_request.status = ProcurementStatus.PENDING
processing_request.save(update_fields=["status", "updated_at"])
Approval.objects.create(
request=processing_request,
approver=processing_request.department.head,
priority=HEAD_PRIORITY,
status=ApprovalStatus.PENDING,
)
api_client.force_authenticate(user=supply_user)
url = reverse(
'api:v1:procurement:procurementrequest-submit',
kwargs={'pk': processing_request.id},
)
response = api_client.post(url)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_final_approve_returns_processing_request_to_waiting_without_executor(
self, api_client, supply_user, department_head, processing_request,
procurement_item_factory, department_head_approval_route
):
procurement_item_factory(request=processing_request)
submit_url = reverse(
'api:v1:procurement:procurementrequest-submit',
kwargs={'pk': processing_request.id},
)
approve_url = reverse(
'api:v1:procurement:procurementrequest-approve',
kwargs={'pk': processing_request.id},
)
api_client.force_authenticate(user=supply_user)
submit_response = api_client.post(submit_url)
assert submit_response.status_code == status.HTTP_200_OK
api_client.force_authenticate(user=department_head)
approve_response = api_client.post(approve_url, {'comment': 'Можно'})
assert approve_response.status_code == status.HTTP_200_OK
processing_request.refresh_from_db()
assert processing_request.status == ProcurementStatus.WAITING
assert processing_request.executor is None
assert processing_request.processing_department is not None
def test_final_approve_returns_processing_request_to_executor_work(
self, api_client, supply_user, department_head, processing_request,
procurement_item_factory, department_head_approval_route
):
procurement_item_factory(request=processing_request)
processing_request.executor = supply_user
processing_request.status = ProcurementStatus.IN_PROGRESS
processing_request.started_at = timezone.now()
processing_request.save(
update_fields=[
"executor",
"status",
"started_at",
"updated_at",
]
)
submit_url = reverse(
'api:v1:procurement:procurementrequest-submit',
kwargs={'pk': processing_request.id},
)
approve_url = reverse(
'api:v1:procurement:procurementrequest-approve',
kwargs={'pk': processing_request.id},
)
api_client.force_authenticate(user=supply_user)
submit_response = api_client.post(submit_url)
assert submit_response.status_code == status.HTTP_200_OK
api_client.force_authenticate(user=department_head)
approve_response = api_client.post(approve_url, {'comment': 'Можно'})
assert approve_response.status_code == status.HTTP_200_OK
processing_request.refresh_from_db()
assert processing_request.status == ProcurementStatus.IN_PROGRESS
assert processing_request.executor == supply_user
assert processing_request.processing_department is not None
def test_reject_processing_request_stops_purchase(
self, api_client, supply_user, department_head, processing_request,
procurement_item_factory, department_head_approval_route
):
procurement_item_factory(request=processing_request)
submit_url = reverse(
'api:v1:procurement:procurementrequest-submit',
kwargs={'pk': processing_request.id},
)
reject_url = reverse(
'api:v1:procurement:procurementrequest-reject',
kwargs={'pk': processing_request.id},
)
api_client.force_authenticate(user=supply_user)
submit_response = api_client.post(submit_url)
assert submit_response.status_code == status.HTTP_200_OK
api_client.force_authenticate(user=department_head)
reject_response = api_client.post(reject_url, {'comment': 'Нельзя'})
assert reject_response.status_code == status.HTTP_200_OK
processing_request.refresh_from_db()
assert processing_request.status == ProcurementStatus.REJECTED
def test_pending_processing_request_blocks_execution_actions(
self, api_client, supply_user, processing_request,
procurement_item_factory, department_head_approval_route
):
item = procurement_item_factory(request=processing_request)
processing_request.status = ProcurementStatus.PENDING
processing_request.save(update_fields=["status", "updated_at"])
Approval.objects.create(
request=processing_request,
approver=processing_request.department.head,
priority=HEAD_PRIORITY,
status=ApprovalStatus.PENDING,
)
api_client.force_authenticate(user=supply_user)
item_url = reverse(
'api:v1:procurement:procurementitem-detail',
kwargs={'pk': item.id},
)
report_issue_url = reverse(
'api:v1:procurement:procurementitem-report-issue',
kwargs={'pk': item.id},
)
mark_all_url = reverse(
'api:v1:procurement:procurementrequest-mark-all-received',
kwargs={'pk': processing_request.id},
)
complete_url = reverse(
'api:v1:procurement:procurementrequest-complete',
kwargs={'pk': processing_request.id},
)
start_url = reverse(
'api:v1:procurement:procurementrequest-start-work',
kwargs={'pk': processing_request.id},
)
item_response = api_client.patch(
item_url,
{'execution_status': ProcurementItemExecutionStatus.RECEIVED},
format='json',
)
issue_response = api_client.post(
report_issue_url,
{'text': 'Брак'},
format='json',
)
mark_all_response = api_client.post(mark_all_url)
complete_response = api_client.post(complete_url)
start_response = api_client.post(start_url)
assert item_response.status_code == status.HTTP_403_FORBIDDEN
assert issue_response.status_code == status.HTTP_400_BAD_REQUEST
assert mark_all_response.status_code == status.HTTP_400_BAD_REQUEST
assert complete_response.status_code == status.HTTP_403_FORBIDDEN
assert start_response.status_code == status.HTTP_400_BAD_REQUEST
def test_item_status_recalculates_request_fulfillment_status(
self, api_client, supply_user, processing_request,
procurement_item_factory

View File

@ -155,6 +155,7 @@ function ProcurementRequestActionButtons({
const canStartWork = (status === "approved" || status === "waiting") && !request.executor;
const isInProgress = status === "in_progress";
const canApproveThis = Boolean(request.can_current_user_approve);
const canSubmitForApproval = Boolean(request.can_current_user_submit_for_approval);
const buttonClass = (variantClass: string) =>
`${variantClass} inline-flex h-9 items-center justify-center rounded-lg disabled:opacity-60 ${
showLabels ? "gap-1.5 px-3 text-xs font-medium" : "w-9"
@ -163,16 +164,16 @@ function ProcurementRequestActionButtons({
return (
<div className="flex flex-wrap items-center gap-1.5 pt-1">
{isDraft && isAuthor ? (
{canSubmitForApproval ? (
<button
type="button"
onClick={() => onSubmit(request.id)}
disabled={busyKey === `submit-${request.id}`}
title="На согласование"
title="Отправить на согласование"
className={buttonClass("app-action-primary")}
>
<Send size={14} />
{label("На согласование")}
{label("Отправить на согласование")}
</button>
) : null}
{showSecondaryActions && isDraft && isAuthor ? (
@ -310,7 +311,6 @@ export default function ProcurementPage() {
handleLoadMore,
handleReject,
handleSave,
handleSaveAndSubmit,
handleStart,
handleSubmit,
isFinal,
@ -1247,22 +1247,10 @@ export default function ProcurementPage() {
type="button"
onClick={handleSave}
disabled={busyKey === "save" || busyKey === "save-submit"}
className={`${modalMode === "create" && form.requireApproval ? "app-action-secondary" : "app-action-primary"} rounded-lg px-3 py-2 text-sm font-medium disabled:opacity-60`}
className="app-action-primary rounded-lg px-3 py-2 text-sm font-medium disabled:opacity-60"
>
{modalMode === "create"
? form.requireApproval ? "Создать черновик" : "Создать и направить в отдел"
: "Сохранить"}
{modalMode === "create" ? "Создать заявку" : "Сохранить"}
</button>
{modalMode === "create" && form.requireApproval ? (
<button
type="button"
onClick={handleSaveAndSubmit}
disabled={busyKey === "save" || busyKey === "save-submit"}
className="app-action-primary rounded-lg px-3 py-2 text-sm font-medium disabled:opacity-60"
>
Отправить на согласование
</button>
) : null}
</div>
}>
@ -1283,36 +1271,17 @@ export default function ProcurementPage() {
{/* Описание */}
<div>
<label className="app-text-muted mb-1 block text-xs font-medium">
Описание и обоснование{form.requireApproval ? " *" : " (необязательно)"}
Описание и обоснование (необязательно)
</label>
<textarea
value={form.description}
onChange={(e) => setForm((f) => ({ ...f, description: e.target.value }))}
placeholder={form.requireApproval ? "Обоснуйте необходимость закупки..." : "Можно оставить пустым и описать детали в позициях..."}
placeholder="Можно оставить пустым и описать детали в позициях..."
rows={3}
className="app-input w-full rounded-lg px-3 py-2 text-sm"
/>
</div>
<label className="app-surface-muted flex items-start gap-3 rounded-xl px-3 py-3 text-sm">
<input
type="checkbox"
checked={form.requireApproval}
onChange={(e) => setForm((f) => ({
...f,
requireApproval: e.target.checked,
processing_department: e.target.checked ? null : f.processing_department,
}))}
className="mt-0.5 h-4 w-4 rounded border-gray-300 text-sky-600"
/>
<span>
<span className="block font-medium text-[var(--foreground)]">Требовать согласование</span>
<span className="app-text-muted mt-0.5 block text-xs">
Если выключено, заявка сразу попадет в очередь выбранного отдела.
</span>
</span>
</label>
{/* Отдел + Срочность */}
<div className="grid grid-cols-2 gap-3">
<SearchableSelectSingle
@ -1337,16 +1306,14 @@ export default function ProcurementPage() {
</div>
</div>
{!form.requireApproval ? (
<SearchableSelectSingle
label="Отдел-исполнитель *"
placeholder="Выберите отдел, который обработает заявку..."
items={departments.map((d) => ({ id: d.id, name: d.name }))}
selectedId={form.processing_department}
onSelect={(id) => setForm((f) => ({ ...f, processing_department: id }))}
disabled={Boolean(defaultProcessingDepartmentId)}
/>
) : null}
<SearchableSelectSingle
label="Отдел-исполнитель *"
placeholder="Выберите отдел, который обработает заявку..."
items={departments.map((d) => ({ id: d.id, name: d.name }))}
selectedId={form.processing_department}
onSelect={(id) => setForm((f) => ({ ...f, processing_department: id }))}
disabled={modalMode === "create" && Boolean(defaultProcessingDepartmentId)}
/>
{/* ── Items ── */}
<div>

View File

@ -523,10 +523,11 @@ export function ProcurementRequestDetailContent({
footer,
}: ProcurementRequestDetailContentProps) {
const canEditItemProcessing = Boolean(canProcessItems && onUpdateItem);
const isRequestPending = String(request.status || "").toLowerCase() === "pending";
const requestorId = typeof request.requestor === "number" ? request.requestor : request.requestor?.id ?? null;
const executorId = typeof request.executor === "number" ? request.executor : request.executor?.id ?? null;
const canReportIssues = Boolean(
onReportItemIssue && currentUserId && (
!isRequestPending && onReportItemIssue && currentUserId && (
canEditItemProcessing ||
currentUserId === requestorId ||
currentUserId === executorId

View File

@ -51,7 +51,7 @@ const emptyForm: FormState = {
title: "",
description: "",
department: null,
requireApproval: true,
requireApproval: false,
processing_department: null,
urgency: "medium",
items: [{ ...emptyItem }],
@ -335,7 +335,11 @@ export function useProcurementPage(user: User | null) {
}, [departments]);
useEffect(() => {
if (form.requireApproval || !defaultProcessingDepartmentId) {
if (
editingId !== null ||
form.requireApproval ||
!defaultProcessingDepartmentId
) {
return;
}
@ -351,7 +355,7 @@ export function useProcurementPage(user: User | null) {
processing_department: defaultProcessingDepartmentId,
};
});
}, [defaultProcessingDepartmentId, form.requireApproval]);
}, [defaultProcessingDepartmentId, editingId, form.requireApproval]);
const filteredRequests = useMemo(() => {
const urgencyRank: Record<string, number> = {
@ -401,7 +405,7 @@ export function useProcurementPage(user: User | null) {
title: detail.title || "",
description: detail.description || "",
department: detail.department ?? null,
requireApproval: !detail.processing_department,
requireApproval: false,
processing_department: detail.processing_department ?? null,
urgency: detail.urgency || "medium",
items: detail.items && detail.items.length > 0
@ -454,24 +458,20 @@ export function useProcurementPage(user: User | null) {
return detail;
}, []);
const saveRequest = useCallback(async (submitAfterCreate = false) => {
const saveRequest = useCallback(async () => {
try {
setBusyKey(submitAfterCreate ? "save-submit" : "save");
setBusyKey("save");
setActionError(null);
if (!form.title.trim()) {
setActionError("Укажите название заявки.");
return;
}
if (form.requireApproval && !form.description.trim()) {
setActionError("Укажите описание и обоснование.");
return;
}
if (!form.department) {
setActionError("Выберите отдел.");
return;
}
if (!form.requireApproval && !form.processing_department) {
if (!form.processing_department) {
setActionError("Выберите отдел-исполнитель.");
return;
}
@ -498,7 +498,7 @@ export function useProcurementPage(user: User | null) {
title: form.title,
description: form.description.trim(),
department: form.department,
processing_department: form.requireApproval ? null : form.processing_department,
processing_department: form.processing_department,
urgency: form.urgency,
items: validItems.map((item) => ({
name: item.name,
@ -513,13 +513,8 @@ export function useProcurementPage(user: User | null) {
};
if (modalMode === "create") {
const created = await apiClient.createProcurementRequest(payload) as ProcurementRequest;
if (submitAfterCreate && form.requireApproval && created.id) {
await apiClient.submitProcurementRequest(created.id);
setActionSuccess("Заявка создана и отправлена на согласование.");
} else {
setActionSuccess(form.requireApproval ? "Заявка создана (черновик)." : "Заявка направлена в отдел.");
}
await apiClient.createProcurementRequest(payload) as ProcurementRequest;
setActionSuccess("Заявка создана и направлена в отдел.");
setCreateOpen(false);
} else if (editingId) {
await apiClient.updateProcurementRequest(editingId, {
@ -541,9 +536,7 @@ export function useProcurementPage(user: User | null) {
}
}, [editingId, form, loadPage1, modalMode, resetForm]);
const handleSave = useCallback(() => saveRequest(false), [saveRequest]);
const handleSaveAndSubmit = useCallback(() => saveRequest(true), [saveRequest]);
const handleSave = useCallback(() => saveRequest(), [saveRequest]);
const doAction = useCallback(async (key: string, action: () => Promise<unknown>, id: number, successMessage: string) => {
try {
@ -957,7 +950,6 @@ export function useProcurementPage(user: User | null) {
modalMode,
isModalOpen,
handleSave,
handleSaveAndSubmit,
handleLoadMore,
toggleExpand,
toggleComments,

View File

@ -561,6 +561,7 @@ export interface ProcurementRequest {
total_received_quantity?: number;
comments_count?: number;
can_current_user_approve?: boolean;
can_current_user_submit_for_approval?: boolean;
created_at: string;
updated_at: string;
submitted_at?: string | null;