Заявки на оборудование: роли и разделение обязанностей
Это описание ролей в equipment request workflow: кто создает заявку, кто согласует деньги, кто планирует обеспечение и почему backend не запрещает одному человеку совмещать несколько ролей.
Общая схема
Equipment Request — фасад над логистическими таблицами для маршрута PM -> финансы -> закупки/логистика.
Основные endpoint-ы:
POST /api/v1/projects/:id/equipment-requests— проектная заявка от менеджера;POST /api/v1/equipment-requests— standalone-заявка;POST /api/v1/equipment-requests/:id/submit— отправить на финансы;POST /api/v1/equipment-requests/:id/finance/approve|reject— финансовое решение;POST /api/v1/equipment-requests/:id/procurement/start|reject— решение закупок/логистики;GET /api/v1/equipment-requests/:id/source-options— варианты источников;POST /api/v1/equipment-requests/:id/operation-plan/preview|confirm— план и создание дочерних операций.
Фасад возвращает агрегированный status, но внутри также хранит logistics_status, finance_status и procurement_status.
Одна позиция оборудования — одна строка заявки. Повтор equipment_id в items[] не ошибка: количество складывается в первую строку. Проектная заявка, созданная при открытой черновой заявке проекта (request_created), доливается в неё: для того же оборудования requested_qty прибавляется к строке, project_requested_qty, дни аренды, цена и комментарий берутся из нового запроса. Потребность проекта (PUT /projects/:id/equipment-requirements/:equipment_id) по-прежнему задаёт количество целиком — строка на оборудование в проекте одна (уникальность по проекту и оборудованию).
Роли в процессе
| Роль | Что делает | Основные поля |
|---|---|---|
| Инициатор | Создает заявку и позиции оборудования | requested_by, items[] |
| Финансовый согласующий | Проверяет сумму и принимает решение | finance_status, finance_decided_by, finance_decided_at |
| Закупки/логистика | Берет заявку в работу, выбирает источники, строит план операций | procurement_status, responsible_id, source-options |
| Логист маршрута | Ведет дочерние logistic_operations до исполнения и закрытия | confirmed_by, confirmed_at, carrier/driver/date fields |
Это процессные роли, а не обязательно разные штатные должности.
Кто что может (решение «каждый отвечает за своё»)
| Действие | Кто | Отказ |
|---|---|---|
| Создать, править (позиции, потребности проекта), отправить, отменить заявку проекта; импорт списка | Участники команды проекта и те, кто его ведёт (projects.Service.CanManageProject: запись карточки «все», «свой» в своём дивизионе, руководитель проекта), плюс склад и логистика (transport.RunsLogistics: logistics.manage или запись equipment_stock) и глобальный администратор | 403 equipment_requests_write_forbidden — например, руководитель дивизиона или менеджер в чужом проекте |
| Архивировать заявку проекта | Те, кто ведёт проект, склад и логистика — на любом этапе; участник команды — только черновик (created, до отправки) | 403 equipment_requests_archive_submitted_forbidden |
Отклонить заявку (/reject) | Те, кто ведёт проект, склад и логистика, глобальный администратор | 403 equipment_requests_reject_forbidden |
| Читать заявки проекта, карточку и итоги заявки | Раздел логистики проекта, склад и логистика, финансы (transport.IsFinance) — им согласовывать суммы | 403 |
| Завести при импорте новую позицию каталога | Склад (запись equipment_stock), инженеры (ENGINEER, TECH_DIR), equipment.manage, глобальный администратор (transport.ManagesEquipmentCatalog) | Строка уходит в errors[] импорта: «Оборудования „…“ нет в каталоге — попросите склад добавить позицию» |
finance/approve, finance/reject | Только финансы: запись затрат costs «все» (OWNER, OPS, FIN_DIR) или глобальный администратор (transport.IsFinance). Менеджер с затратами «свой» свою заявку не согласует | 403 equipment_requests_finance_decision_forbidden |
Закупка (procurement/*), план операций, автоплан | Только склад и логистика (transport.RunsLogistics); раздела логистики проекта для этого недостаточно | 403 equipment_requests_procurement_forbidden |
Очередь заявок (GET /equipment-requests) | Склад и логистика, финансы | 403 equipment_requests_logistics_desk_required |
| Заявки без проекта (складские) | Склад и логистика | 403 equipment_requests_logistics_desk_required |
Правило записи собрано в internal/app/project_supply_access.go и отдаётся карточке проекта в access.equipment_requests; интерфейс по нему показывает кнопки заявки, по capability finance_approve — кнопки согласования суммы, по equipment_catalog_manage — подсказку про новые позиции при импорте, по logistics_manage (= logistics_operate) — очередь, закупку и план операций.
Маршрут заявки
created
submit
↓
under_review
finance approve
↓
under_review / awaiting_supply
procurement start
↓
in_progress
operation-plan confirm
↓
in_transit / completedОсновные переходы:
| Действие | Проверка | Результат |
|---|---|---|
submit | status=created, finance_status=draft | logistics_status=under_review, finance_status=pending |
finance/approve | finance_status=pending, logistics_status=under_review | finance_status=approved, logistics_status=awaiting_supply |
finance/reject | заявка ожидает финрешение | finance_status=rejected, logistics_status=cancelled |
procurement/start | финансы approved | procurement_status=in_progress, logistics_status=sourced |
procurement/reject | финансы approved | procurement_status=rejected, logistics_status=cancelled |
operation-plan/confirm | для проектной заявки финансы approved | создаются дочерние logistic_operations |
API-статусы фасада:
created;under_review;in_progress;in_transit;completed;rejected;cancelled.
Почему один человек может делать несколько шагов
Backend намеренно не зашивает жесткое правило "инициатор не может согласовать свою заявку".
Причина практическая: в маленькой команде один сотрудник может быть и менеджером, и финансистом, и логистом. Жесткое segregation of duties сделало бы такой сценарий невозможным.
Вместо запрета система фиксирует подотчетность:
- Финансовое решение записывает
finance_decided_by,finance_decided_atи комментарий. - Procurement-решение записывает
procurement_decided_by,procurement_decided_atи комментарий. - Подтверждение маршрута записывает
confirmed_byиconfirmed_at. - Переходы выполняются через ожидаемые текущие статусы; повторный клик или гонка операторов не должны применить решение дважды.
Если организации нужна строгая раздельность, она настраивается правами: не выдавайте одному пользователю одновременно права инициирования, финансового согласования и procurement/logistics-действий.
Деньги в заявке
Стоимость позиций считается по equipment.unit_cost * requested_qty * day_multiplier.
rental_days задается на позиции заявки. Если клиент его не передал, используется 1.
Множитель по дням:
- 1-й день — 100%;
- 2-5 дни — 50% за день;
- 6-10 дни — 30% за день;
- 11-15 дни — 15% за день;
- 16+ дни — 10% за день.
Денежная арифметика должна выполняться точно, через правила из Денежных сумм.
Поведение фронта
- Показывайте пользователю агрегированный
status, но для диагностики сохраняйте raw поляlogistics_status,finance_status,procurement_status. - Кнопку
submitпоказывайте только дляcreated. - Финансовые кнопки показывайте только для
finance_status=pending. - Procurement-действия показывайте после
finance_status=approved. - Планирование маршрутов для проектной заявки запускайте только после finance approve.
- После
operation-plan/confirmоткрывайте список дочерних операций черезGET /api/v1/equipment-requests?parent_request_id=<id>&include_items=true.
Подробный сценарий source options и дочерних логистических операций описан в Логистическом контуре.