Логистический контур: как работает подбор оборудования
Это описание того, как в системе работает процесс получения оборудования для проекта — от момента, когда менеджер понимает, что что-то нужно, до момента, когда оборудование доставлено и принято.
Общая схема
В текущей версии backend есть три связанных слоя:
/api/v1/projects/:id/equipment-requestsи/api/v1/equipment-requests/:id/...— фасад заявки на оборудование для маршрута PM → финансы → закупки/логистика./api/v1/equipment-requests/:id/source-optionsи/api/v1/equipment-requests/:id/operation-plan/...— рабочее место логиста: подсказки источников, preview маршрутов и подтверждение создания операций./api/v1/logistics— низкоуровневая логистическая операция, которая хранит исполнение, перемещения, перевозчика, водителя, фактические даты и затраты.
Заявки и операции используют logistic_operations и logistic_operation_items. Проектная потребность хранится отдельно в project_equipment_requirements: это авторитетное количество, которое нужно проекту по паре project_id + equipment_id. Фасад заявки добавляет workflow-поля финансового и procurement-решения, а планирование связывает исходную заявку с созданными операциями через parent_request_id.
Правила текущей версии
- Основной склад определяется backend-ом как локация
name="СКЛАД КАЗАНЬ"иtype="warehouse". - Если оборудование физически лежит на основном складе, оно считается свободным от проектной занятости:
project_idиstatus=in_use/reservedсами по себе не делают его недоступным. - Активные резервы дочерних логистических операций всегда вычитаются, включая основной склад, чтобы не забронировать одну и ту же позицию дважды.
- Родительская заявка менеджера (
request_kind=project_equipment,parent_request_id=null) не является физическим резервом. - Отмена, архивирование или удаление строк заявки не сбрасывает
project_equipment_requirements.required_qty; это разные уровни данных: потребность проекта и история логистического исполнения. - Проект считается обеспеченным оборудованием только после закрытия дочерней операции (
status=closed) или если нужное количество уже физически есть на целевой точке. - Отмена/отклонение родительской заявки архивирует ее и скрывает из активных списков, но оставляет историю и аудит.
Логистическая операция — это не абстрактная заявка, а конкретная доставка от одной точки до другой:
from_location_id— откуда забираем груз;to_location_id— куда доставляем груз;items[]— какое оборудование и в каком количестве везем; в ответе у позиции естьequipment_name(название из справочника оборудования), чтобы фронт не догружал каталог ради подписи;fulfillment_type— внутренний трансфер, закупка или аренда;carrier_type,driver_id,carrier, даты и адреса — параметры исполнения маршрута.
Текущая локация оборудования хранится в equipment.location_id. Фронт не должен заставлять пользователя угадывать исходную локацию: сначала выбирается оборудование и количество, затем source-options показывает доступные склады/проекты/закупку/аренду, и только после выбора источника формируется маршрут операции.
Менеджер: "Для проекта нужны стулья и столы"
↓
Система: "Проверяю наличие на складе..."
↓
Система: "На складе 5 стульев, но нужно 10. На столах все в порядке."
↓
Менеджер: "Создаю заявку на оборудование"
↓
Логистика: "Беру в работу. Буду искать 5 дополнительных стульев."
↓
Система: "Показываю источники: склад, другой проект, покупка, аренда"
↓
Логистика: "Выбираю источники и формирую операции по маршрутам"
↓
Финансы: "Выделяю деньги под известную сумму логистики"
↓
Система: "Создаю отдельные операции для разных локаций и типов обеспечения"
↓
Логистика: "Назначаю перевозчика, водителя, адреса и даты"
↓
Логистика: "Товар в пути и доставлен на объект"
↓
Менеджер: "Проверил, все в порядке, принимаю товар"
↓
Система: "Стулья теперь на проекте, затраты учтены в бюджете"Этап 1: Определение потребностей
Как начинается логистический процесс
Обычный сценарий:
- Менеджер проекта смотрит на требования из ТЗ.
- Понимает, какое оборудование/материалы нужны.
- Составляет список в системе (или вручную).
- Проверяет наличие в системе.
Пример:
Проект: Event Setup
Нужны:
- Стулья: 10 шт
- Столы: 3 шт
- Проектор: 1 шт
- Кабельная система: 1 комплКак система помогает
Система показывает наличие:
Стулья (тип "офисный"):
Total Stock: 25 шт
Reserved on other projects: 10 шт
Available for this project: 15 шт
→ ВЫВОД: Хватает! (нужно 10, есть 15)
Столы (тип "журнальный"):
Total Stock: 8 шт
Reserved on other projects: 2 шт
Available for this project: 6 шт
→ ВЫВОД: Хватает! (нужно 3, есть 6)
Проектор (модель "Epson EB-X39"):
Total Stock: 3 шт
Reserved on other projects: 3 шт
Available for this project: 0 шт
→ ВЫВОД: НЕ ХВАТАЕТ! (нужен 1, а нет ни одного)
Кабельная система:
Total Stock: 2 компл
Reserved on other projects: 1 компл
Available for this project: 1 компл
→ ВЫВОД: Хватает! (нужен 1, есть 1)Создание заявки на оборудование
На основе анализа менеджер создает Equipment Request. Backend сохраняет ее как logistic_operations + logistic_operation_items, но фронту отдает отдельный контракт заявки:
type=transfer→fulfillment_type=internal_transfertype=purchase→fulfillment_type=purchasetype=rental→fulfillment_type=rentaltype=unspecified→fulfillment_type=NULL
Минимальный payload для создания:
{
"name": "Equipment for Event Setup",
"items": [
{
"equipment_id": 10,
"requested_qty": 2
}
]
}Обязательные поля: name, items[], внутри позиции — equipment_id и requested_qty. Логистические поля (from_location_id, to_location_id, planned_departure, planned_arrival) при создании опциональны. Их можно заполнить позже, когда закупки/логистика берут заявку в работу.
Если дата не заполнена на фронте, поле лучше не отправлять. planned_departure и planned_arrival также можно передать как null или "". Для заполненных дат основной формат — RFC3339 (2026-06-15T09:00:00Z); backend также принимает HTML-форматы date и datetime-local (2026-06-15, 2026-06-15T09:00).
При создании система считает planned_cost как сумму equipment.unit_cost * requested_qty * day_multiplier (cost остается совместимым алиасом в API). rental_days задается на позиции заявки и по умолчанию равен 1, если клиент его не передал. Если у позиции нет цены, позиция остается в заявке, но ее вклад в плановую стоимость считается как 0.
day_multiplier считается прогрессивно:
- 1-й день — 100% цены.
- Со 2-го по 5-й день — 50% цены за каждый день.
- С 6-го по 10-й день — 30% цены за каждый день.
- С 11-го по 15-й день — 15% цены за каждый день.
- С 16-го дня и далее — 10% цены за каждый день.
Backend также заполняет количественные поля позиции:
reserved_qty— сколько можно зарезервировать из текущего доступного остатка.shortage_qty— сколько не хватает и нужно закрыть закупкой, арендой или трансфером.rental_days— на сколько дней аренды/использования считается стоимость позиции.
Equipment Request: "Equipment for Event Setup"
Name: "Equipment for Event Setup"
Project: Event Setup
Type: purchase
Items:
1. Chairs (office) - Qty: 10, Days: 2
→ Reserved on warehouse: 10 (available)
→ Planned cost: unit_cost * 10 * (1 * 100% + 1 * 50%)
→ SourceType: warehouse
→ Status: Ready to pick up
2. Tables (journal) - Qty: 3
→ Reserved on warehouse: 3 (available)
→ SourceType: warehouse
→ Status: Ready to pick up
3. Projector - Qty: 1
→ Available: 0, needs to source
→ SourceType: to_be_determined
→ Status: Need to find
4. Cable system - Qty: 1
→ Reserved on warehouse: 1 (available)
→ SourceType: warehouse
→ Status: Ready to pick up
API Status: "created"
Logistics Status: "request_created"
Finance Status: "draft"
Procurement Status: "not_started"Создание логистической операции не блокируется финансами. Финансы подключаются только когда у операции есть конкретная planned_cost: при создании с суммой или после того, как логист проработал маршрут и подтвердил операцию с суммой. До финансового approve проектную операцию с суммой нельзя переводить в исполнение (preparing_shipment, in_transit и дальше), но ее можно создать и уточнять.
Финансовое состояние хранится отдельно от логистического status:
| finance_status | Когда используется |
|---|---|
amount_missing | Операция создана, сумма еще неизвестна |
Если сумму вписали позже (правкой или при подтверждении), amount_missing сразу сменяется на pending; уже сохранённые записи исправила миграция 0151. Согласование финансов обязательно только операциям проекта, поэтому у операций без проекта интерфейс показывает «Не требуется». | pending | Сумма указана и ожидает обработки финансами | | approved | Деньги выделены, логистика может вести исполнение | | rejected | Сумма отклонена, операция остается для корректировки | | draft | Legacy/технический черновик для совместимости |
Финансовые решения по низкоуровневой операции выполняются через:
POST /api/v1/logistics/:id/finance/approve;POST /api/v1/logistics/:id/finance/reject.
Автоплан заявки
Для нового flow фронт может не собирать маршруты вручную. Backend сам строит план:
POST /api/v1/equipment-requests/:id/auto-plan/preview
POST /api/v1/equipment-requests/:id/auto-plan/confirmpreview ничего не пишет в БД. confirm создает дочерние операции через основной /logistics service, поэтому работают задачи, аудит, финансовый gate и синхронизация затрат.
Порядок закрытия потребности:
- Засчитывается оборудование, которое уже есть на точке доставки — кроме заявок «довезти» из карточки проекта: их количество уже за вычетом площадки (у позиции
project_requested_qtyбольшеrequested_qty), и лежащее там второй раз не вычитается. - Затем берется основной склад
СКЛАД КАЗАНЬ. - Затем другие склады и проектные точки.
- Если внутреннего наличия не хватает, draft purchase создается только при
allow_purchase=true, draft rental — только приallow_rental=true. - Если source locations разные, backend создает разные операции: например
А -> БиВ -> Б.
Пример payload:
{
"to_location_id": 2,
"responsible_id": 44,
"carrier_type": "own_driver",
"driver_id": 55,
"vehicle_number": "А123ВС 116",
"planned_departure": "2026-06-20T09:00:00Z",
"planned_arrival": "2026-06-20T18:00:00Z",
"allow_purchase": true,
"allow_rental": true,
"items": [
{ "request_item_id": 10, "quantity": 7, "unit_price": 45000 }
]
}items[].unit_price — цена единицы закупки или аренды, которую логист вводит в мастере. Она сохраняется в позиции операции (logistic_operation_items.unit_price) и прибавляется к сумме операции: финансы согласуют доставку вместе с закупкой/арендой (planned_cost = доставка + цена × количество).
routes[] — отдельный маршрут на каждую группу операций, когда план смешанный (склад везёт своё, недостающее арендуется или закупается). Маршрут выбирается по fulfillment_type (internal_transfer / rental / purchase), для трансфера — ещё по from_location_id (маршрут без точки подходит любому складу). У маршрута свои перевозчик, водитель, машина, трек-номер, даты и planned_cost доставки; сумма доставки маршрута достаётся одной операции его группы. Поля маршрута перекрывают общие поля payload; группы без маршрута берут общие. Сервер проверяет каждый маршрут так же, как общий: прибытие не раньше отправки, допустимый тип перевозчика, у own_driver — водитель, сумма неотрицательная.
"routes": [
{ "fulfillment_type": "internal_transfer", "from_location_id": 1, "carrier_type": "own_driver", "driver_id": 55, "planned_cost": 5000 },
{ "fulfillment_type": "rental", "carrier_type": "supplier_delivery", "carrier": "ООО Прокат-Звук", "planned_cost": 3000 }
]Если items не переданы, backend планирует все активные позиции заявки. to_location_id берется из payload, затем из заявки, затем из project.location_id; если точка не найдена, API вернет validation error.
Ответ preview/confirm содержит coverage[] и operations[]. Во фронте coverage.uncovered_qty > 0 означает, что часть потребности не закрыта и нужно разрешить закупку/аренду или изменить количество.
Взять в работу и архивировать
Логист может назначить ответственного:
POST /api/v1/logistics/:id/take{ "responsible_id": 44 }Если responsible_id не передан, используется текущий пользователь.
Для родительских заявок:
POST /api/v1/equipment-requests/:id/archive
POST /api/v1/equipment-requests/:id/reject{ "comment": "Нет источников, можно подать заново позже" }archive/reject скрывают заявку из активных списков. При cancel, если дочерних операций нет, заявка архивируется сразу; если операции есть, заявка переходит в cancellation_requested и архивируется после отмены всех дочерних операций.
Инструкция для фронтенда: создание операции и финансовое согласование
Эта секция — самодостаточный гайд: что дёргать, с какими полями и в каких случаях. Главное правило: создание логистической операции никогда не блокируется финансами. Финансовое согласование — это отдельный шаг по операции, который запускается автоматически, как только у операции появляется сумма.
Кто что делает
- Логист создаёт операции, проставляет маршрут, перевозчика, даты и
planned_cost, ведёт исполнение. - Финансист только согласует сумму (approve/reject) по операции. Он не создаёт операции.
Кто вправе создать операцию (POST /logistics, POST /logistics/auto-shortage):
| Кто | Какие операции | Отказ |
|---|---|---|
Склад и логистика: logistics.manage или запись equipment_stock (transport.RunsLogistics), глобальный администратор | Любые, в том числе без проекта | — |
Руководитель проекта и те, кто ведёт проект (projects.Service.CanManageProject) | Только по своему проекту; project_id обязателен | 400 logistics_create_project_required без проекта, 403 logistics_create_project_forbidden в чужом проекте |
| Остальные | — | 403 |
Вести операцию (PATCH, DELETE, take, confirm, close) может тот же круг: склад и логистика — любую, те, кто ведёт проект, — операцию своего проекта. Раздела логистики проекта (роль в команде) для записи недостаточно — 403 logistics_write_forbidden. Блокировки состояния (закрытую операцию не правят, исполнение до согласования финансов, удаление без force=true) — 409.
Читать операцию и очередь без project_id могут ещё и финансы (transport.IsFinance): они согласуют суммы.
Согласовать сумму (/logistics/:id/finance/approve|reject) могут только финансы: запись затрат «все» или глобальный администратор (transport.IsFinance), иначе 403 logistics_finance_decision_forbidden. Менеджер с затратами «свой» и legacy finance.manage свою перевозку больше не согласует. Карточка проекта отдаёт access.logistics_create, /org/me — capabilities logistics_operate и finance_approve.
Эндпоинт создания
POST /api/v1/logisticsМинимальный payload — операцию можно создать черновиком, без маршрута и без суммы:
{ "name": "Доставка на объект" }Полный payload (все поля опциональны, кроме name):
| Поле | Тип | Когда нужно |
|---|---|---|
name | string | обязательно |
project_id | int | если операция привязана к проекту (включает финансовый и задачный контур) |
responsible_id | int | логист-исполнитель; на него вешается логистическая задача |
from_location_id / to_location_id | int | откуда/куда; не могут совпадать |
fulfillment_type | enum | internal_transfer / purchase / rental |
carrier_type | enum | own_driver / courier / transport_company / supplier_delivery / pickup / other |
carrier | string | при courier/transport_company/supplier_delivery |
driver_id / driver_name | int / string | при carrier_type=own_driver нужен один из них |
vehicle_number, tracking_number | string | по необходимости |
pickup_address, delivery_address | string | по необходимости |
planned_departure, planned_arrival | RFC3339 | плановые даты; arrival ≥ departure |
planned_cost | number | плановая стоимость логистики (см. ниже про задачу финансисту) |
items[] | array | { equipment_id, requested_qty, rental_days?, source_type?, comment? } |
notes | string | комментарий |
Что произойдёт с финансами при создании
Поведение зависит только от planned_cost и project_id в момент создания:
| Сценарий при создании | finance_status операции | Авто-задача |
|---|---|---|
есть project_id и planned_cost | pending | задача финансисту проекта: «Финансы: обработать логистическую операцию» |
есть project_id, нет planned_cost | amount_missing | логистическая задача на ведение операции |
нет project_id | amount_missing | задачи нет (модель задач требует проект) |
То есть «создать операцию с суммой → сразу поставить финансовому отделу задачу» — это просто POST /api/v1/logistics с заполненным planned_cost и project_id. Никакого предварительного approve не требуется.
Если сумму добавляют позже
Сумму можно проставить или изменить уже у существующей операции:
PATCH /api/v1/logistics/:id
{ "planned_cost": 12000 }Как только planned_cost появляется (или меняется) у проектной операции, backend сам переводит её в finance_status=pending, сбрасывает прошлое решение и ставит задачу финансисту. Фронт ничего дополнительно дёргать не должен.
Согласование суммы финансистом
Финансист открывает операцию (или приходит из задачи) и решает:
POST /api/v1/logistics/:id/finance/approve { "comment": "ок" }
POST /api/v1/logistics/:id/finance/reject { "comment": "уточните смету" }approve→finance_status=approved, плановая сумма уходит в бюджет проекта (finance.cost_items,source_type=logistic_operation), задача финансиста закрывается, логисту ставится задача на ведение операции.reject→finance_status=rejected; операция остаётся для корректировки, логист меняет сумму и сумма снова уходит наpending.
Эти эндпоинты доступны только при finance_status=pending и требуют project_id + planned_cost у операции.
Решение атомарно: смена статуса, статья затрат, закрытие задачи финансиста и постановка задачи логисту идут одной транзакцией (как и POST /logistics/:id/confirm со своими задачами). Если задачу поставить не удалось — всё откатывается, операция остаётся в pending, а ответ несёт понятный текст:
400 logistics_operation_task_invalid— «Не удалось поставить задачу по логистической операции — изменения не сохранены. Причина: …» (причина — текст модуля задач, например про срок или исполнителя);500 logistics_operation_task_failed— сбой без понятной причины, «…Повторите попытку».
Уведомления о новых задачах уходят только после фиксации транзакции.
Гейт исполнения (единственное место, где финансы что-то блокируют)
Финансы не мешают создавать, редактировать, подтверждать маршрут и собирать источники. Они блокируют только перевод в исполнение. Для проектной операции с суммой статусы preparing_shipment, in_transit, delivered, accepted, completed, closed доступны только после finance_status=approved. Иначе backend вернёт ошибку logistics_finance_approval_is_required_before_logistics_execution (HTTP 403). Это ожидаемо: фронт показывает баннер «ожидает согласования финансов» и не даёт двигать статус дальше.
Создание операций из заявки на оборудование
Альтернативный вход — спланировать операции из заявки проекта:
POST /api/v1/equipment-requests/:id/operation-plan/preview // проверка, без записи
POST /api/v1/equipment-requests/:id/operation-plan/confirm // создаёт дочерние операцииФинансовое согласование заявки для этого больше не требуется (ранее возвращалась ошибка equipment_requests_not_approved_for_logistics — она удалена). Созданные операции получают request_kind=planned_operation и parent_request_id. Дальше с каждой операцией работают так же, как с созданной напрямую: проставляют planned_cost через PATCH /api/v1/logistics/:id → операция уходит на финансовое согласование и порождает задачу финансисту.
Статусы фасада заявки на оборудование
Фасад /api/v1/equipment-requests отдает фронту агрегированный status и сырой logistics_status для совместимости с текущей логистикой. Это отдельный facade поверх тех же таблиц.
Финансовое согласование заявки больше не блокирует планирование логистики. Раньше
operation-plan/confirmтребовал, чтобы заявка проекта была вfinance_status=approved. Теперь логист может спланировать и создать операции на любом этапе заявки (даже до отправки на финансовое согласование). Финансы подключаются на уровне операции: как только у операции появляетсяplanned_cost, она уходит вfinance_status=pendingи порождает задачу финансисту; до approve операцию с суммой нельзя перевести в исполнение, но создавать и прорабатывать — можно. Это поведение идентично прямому контуру/api/v1/logistics.
| API status | Когда используется |
|---|---|
created | Заявка создана, но еще не отправлена на финансовое согласование |
under_review | Заявка на финансовом согласовании или ожидает procurement после approve |
in_progress | Закупки/логистика взяли заявку в работу |
in_transit | Оборудование в пути или доставлено, но еще не закрыто |
completed | Доставка выполнена; после закрытия операции closed фасад тоже возвращает completed |
rejected | Финансы или закупки/логистика отклонили заявку |
cancelled | Операция отменена без workflow-отклонения |
Подсказки источников и формирование операций
Логист открывает заявку на оборудование (на любом этапе, финансовое согласование заявки не требуется) и запрашивает варианты источников:
GET /api/v1/equipment-requests/:id/source-options
Ответ группируется по позициям заявки и показывает, где можно взять оборудование:
warehouse— оборудование доступно на складе или локации.project— оборудование закреплено за другим проектом и может быть источником внутреннего трансфера.purchase— fallback-вариант покупки.rental— fallback-вариант аренды.
Для быстрого складского среза по карточке оборудования доступен:
GET /api/v1/equipment/:id/stock
Ответ показывает суммарное количество, активный резерв, доступное количество и детализацию по складам/локациям для выбранной карточки и одноименного оборудования той же категории.
Логист выбирает источники и собирает план операций:
POST /api/v1/equipment-requests/:id/operation-plan/preview— проверяет план и возвращает операции без записи в БД.POST /api/v1/equipment-requests/:id/operation-plan/confirm— создает дочерниеlogistic_operations.
Одна заявка может породить несколько операций. Разбиение выполняется по маршруту и типу обеспечения:
- разные исходные локации → разные операции;
- разные
fulfillment_type(internal_transfer,purchase,rental) → разные операции; - разные перевозчики, водители или даты → отдельные операции, если логист так спланировал.
Созданные операции получают request_kind=planned_operation и parent_request_id=<id исходной заявки>. В операции можно указать:
carrier_type:own_driver,courier,transport_company,supplier_delivery,pickup,other;carrier,driver_idилиdriver_name;vehicle_number,tracking_number;pickup_address,delivery_address;- плановые даты отправки и прибытия.
Если carrier_type=own_driver, должен быть указан driver_id или driver_name.
Свой водитель — перевозит сама компания: сервер всегда ставит carrier = "RMS" (при создании, правке и подтверждении), присланное значение игнорируется. В интерфейсе поле «Перевозчик» заблокировано.
После создания дочерних операций логист открывает список операций заявки:
GET /api/v1/equipment-requests?parent_request_id=<request_id>&include_items=true
Этот список нужен для экрана подтверждения маршрутов: фронт показывает каждую дочернюю операцию, ее груз, перевозчика, водителя, плановые даты и признаки confirmed_by/confirmed_at. Если confirmed_at пустой, маршрут еще не подтвержден. После подтверждения задача уже создана, но плановые параметры операции можно уточнять до closed.
Редактирование плановых параметров операции выполняется через:
PATCH /api/v1/logistics/:id
До closed можно уточнить маршрут, тип обеспечения, перевозчика, водителя, адреса, плановые даты, плановую стоимость и комментарий. После closed операция не редактируется.
Удаление операции выполняется через:
DELETE /api/v1/logistics/:id
Перед показом модалки удаления фронт должен вызвать:
DELETE /api/v1/logistics/:id?dry_run=true
Ответ:
{
"can_delete": false,
"force_required": true,
"warnings": {
"status": "operation status=in_transit normally requires cancellation instead of deletion"
}
}Обычное удаление без force разрешено для неподтвержденных операций в статусах request_created, under_review, awaiting_supply, sourced, cancelled, exception. Для подтвержденных, исполняемых или завершенных операций backend возвращает предупреждение, и фронт должен требовать явное подтверждение пользователя.
После подтверждения пользователя фронт вызывает:
DELETE /api/v1/logistics/:id?force=true
Backend снимает резервы по позициям, возвращает оборудование в available, удаляет связанную финансовую строку source_type=logistic_operation и затем удаляет операцию. Это принудительное действие используется для исправления ошибочно созданных данных; для штатного бизнес-процесса предпочтительнее отмена статусом.
Минимальный CRUD для экрана операций:
- создать:
POST /api/v1/logisticsдля низкоуровневой операции, включая черновик без маршрута/суммы, илиPOST /api/v1/equipment-requests/:id/operation-plan/confirmдля операции из заявки; - посмотреть список:
GET /api/v1/logistics; - посмотреть карточку:
GET /api/v1/logistics/:id; - обновить:
PATCH /api/v1/logistics/:id; - удалить черновик:
DELETE /api/v1/logistics/:id; - проверить последствия удаления:
DELETE /api/v1/logistics/:id?dry_run=true; - принудительно удалить после подтверждения:
DELETE /api/v1/logistics/:id?force=true; - подтвердить маршрут:
POST /api/v1/logistics/:id/confirm. - обработать сумму финансами:
POST /api/v1/logistics/:id/finance/approveилиPOST /api/v1/logistics/:id/finance/reject.
Подтверждение маршрута выполняется отдельным действием:
POST /api/v1/logistics/:id/confirm
Для подтверждения операция должна иметь исходную и целевую локации, fulfillment_type, carrier_type, плановую отправку и плановое прибытие. Самостоятельная операция (request_kind=standalone) без позиций не подтверждается: 400 logistics_operation_has_no_items. Для own_driver нужен driver_id или driver_name; для courier, transport_company и supplier_delivery нужен carrier.
При подтверждении backend заполняет confirmed_by и confirmed_at. Если проектная операция содержит сумму и finance_status еще не approved, подтверждение отправляет операцию в финансы (finance_status=pending) и не переводит ее в исполнение. После финансового approve backend переводит подтвержденную операцию в preparing_shipment.
Если операция уже финансово одобрена или не требует проектного финансового согласования, подтверждение ставит status=preparing_shipment. Если операция привязана к проекту, система создает задачу:
- при
carrier_type=own_driverи заполненномdriver_idзадача назначается водителю; - при внешнем перевозчике задача назначается ответственному логисту (
responsible_id), а если он не указан — пользователю, который подтвердил маршрут.
Standalone-операции без проекта подтверждаются без автоматической задачи, потому текущая модель задач требует project_id.
Standalone-заявки логиста
Логист может создать заявку без проекта:
POST /api/v1/equipment-requests
Такая заявка получает request_kind=standalone, project_id=null и доступна логистам в рабочем столе /api/v1/equipment-requests. Низкоуровневый список /api/v1/logistics без project_id показывает глобальный список только администраторам/глобальным ролям, а обычному пользователю возвращает только операции, где он назначен как driver_id. Это используется для внутренних трансферов между складами, закупок или аренды, которые не привязаны к конкретному проекту.
Глобальный список рабочего стола логистов:
GET /api/v1/equipment-requests
Он требует logistics.manage и может включать дочерние операции через include_children=true. Для экрана операций конкретной заявки используется parent_request_id; если нужен груз в каждой карточке, добавляется include_items=true.
Этап 2: Работа логистики (поиск источников)
Логистика берет заявку в работу
Статус операции: under_review или awaiting_supply (логист уточняет источник, сроки и стоимость)
Логист (или логистический менеджер) получает заявку и начинает анализировать:
Что на складе — все ОК:
- Стулья (10 шт) — берем
- Столы (3 шт) — берем
- Кабельная система (1 компл) — берем
Что нужно найти (Projector):
- Вариант А: Купить новый (поиск поставщика)
- Вариант Б: Арендовать (дешевле, но только на время события)
- Вариант В: Перевезти с другого проекта (если там его освобождают)
Решения, которые логистика может принять
Логистика не задает вопросы менеджеру — она сама принимает решение на основе:
- Бюджета проекта
- Сроков
- Стоимости вариантов
- Наличия ресурсов
Вариант 1: Купить
Логист: "Куплю новый проектор. Есть поставщик X, цена 50,000 руб, доставка 2 дня."
Operation Item: Projector
RequestedQty: 1
ReservedQty: 0
FulfilledQty: 0
ShortageQty: 1
SourceType: "purchase"
SourceReferenceID: <supplier_id>
Comment: "Ordered from Supplier X, model Epson EB-X39, price 50000"Процесс выполнения: Статус остается awaiting_supply до поставки или переводится в sourced, когда источник найден и позиции можно резервировать:
- Заказано (Comment: "Ordered from Supplier X, ETA: 2 дня")
- Получено на склад (Comment: "Received, ready for shipment", FulfilledQty увеличивается)
- Готовится к доставке (Comment: "Packing for shipment")
- Отправлено (когда статус меняется на in_transit)
Вариант 2: Арендовать
Логист: "Буду арендовать на 2 дня. Стоит 5,000 руб в сутки = 10,000 руб."
Operation Item: Projector
RequestedQty: 1
ReservedQty: 0
FulfilledQty: 0
ShortageQty: 1
SourceType: "rental"
SourceReferenceID: <rental_company_id>
Comment: "Rental from Company Y, 2 days @ 5000/day = 10000"Преимущество: Дешевле, чем купить. Если проектор больше не нужен, его возвращаем.
Вариант 3: Перевести с другого проекта
Логист: "Project B заканчивается в пятницу, а нам нужен в субботу. Переместим оттуда."
Operation Item: Projector
RequestedQty: 1
ReservedQty: 0
FulfilledQty: 0 (пока)
ShortageQty: 1
SourceType: "transfer_from_project"
SourceReferenceID: <project_b_id>
Comment: "Will transfer from Project B after their event (Friday)"Процесс выполнения: Статус остается awaiting_supply до освобождения оборудования или переводится в sourced, когда источник готов:
- Ждем освобождения (Comment: "Waiting for Project B to finish by Friday")
- Забрали и готовим (Comment: "Picked up from Project B warehouse")
- Отправлено (когда статус меняется на in_transit, Comment: "In transit to Event Venue")
Обновление заявки в системе
После анализа логист обновляет Logistics Operation со своими решениями:
Operation Status: "sourced" (источник найден, маршрут и стоимость уточнены)
Items:
1. Chairs (10) - SourceType: warehouse, ReservedQty: 10
2. Tables (3) - SourceType: warehouse, ReservedQty: 3
3. Projector (1) - SourceType: rental, ReservedQty: 0 (ждем от rental company)
4. Cable system (1) - SourceType: warehouse, ReservedQty: 1
PlannedCost: 10000 (2 дня аренды проектора)
PlannedDeparture: 2026-04-24 (отправка на объект)
PlannedArrival: 2026-04-25 (доставка на день события)
Comment: "All items sourced. Chairs, tables, cable from warehouse. Projector - rental from Company Y"Этап 3: Выполнение и доставка
Статус операции: preparing_shipment
Логистика выполняет заказ и готовит доставку. Система отслеживает прогресс через комментарии и количественные поля:
Operation Status: "preparing_shipment"
Items:
1. Chairs (10) - ReservedQty: 10, FulfilledQty: 10 ✓
2. Tables (3) - ReservedQty: 3, FulfilledQty: 3 ✓
3. Projector (1) - ReservedQty: 0, FulfilledQty: 1 (rental пришел) ✓
4. Cable system (1) - ReservedQty: 1, FulfilledQty: 1 ✓
PlannedDeparture: 2026-04-24 (день до события)
PlannedArrival: 2026-04-25 10:00 (день события)
Transport: "courier"
Carrier: "DHL Express"
VehicleNumber: "MK-1234-AB"
Comment: "All items assembled and packed. Ready for shipment."Смена статуса на "in_transit"
Когда груз отправляется, статус меняется на in_transit:
Operation Status: "in_transit"
ActualDeparture: 2026-04-24 12:00
TrackingNumber: "DHL-123456789"
PlannedArrival: 2026-04-25 10:00
Comment: "Shipment sent. ETA: 2026-04-25 10:00"Завершение доставки
Менеджер проекта принимает груз и фиксирует выполнение доставки, меняя статус на completed:
Operation Status: "completed"
ActualArrival: 2026-04-25 09:45
TrackingNumber: "DHL-123456789"
Final Items Summary:
1. Chairs (10) - FulfilledQty: 10 ✓
2. Tables (3) - FulfilledQty: 3 ✓
3. Projector (1) - FulfilledQty: 1 ✓ (rental)
4. Cable system (1) - FulfilledQty: 1 ✓
ActualCost: 10000
DifferenceBudget: 0 (совпал с плановым)
Comment: "All items received and verified. Quality OK. Equipment ready for use."После этого логист или ответственный закрывает операцию через POST /api/v1/logistics/:id/close. Только на этом шаге оборудование меняет фактическую локацию.
Отражение в финансах
Логистическая операция попадает в бюджет через Finance Cost Item с:
Category: logistics
SourceType: logistic_operation
SourceID: <operation_id>Создание происходит не при первом сохранении операции. Текущая логика такая:
- операция с
planned_costполучаетfinance_status=pendingи задачу финансисту; POST /api/v1/logistics/:id/finance/approveсоздает или обновляет Cost Item с плановой суммой;- перевод операции в
completedсинхронизирует фактическую сумму, если заполненactual_cost; POST /api/v1/logistics/:id/closeзакрывает операционный цикл и переносит оборудование.
Если аренда или закупка оборудования должна попасть в смету как rent или tech, это отдельная статья finance.cost_items. Логистическая операция отвечает за стоимость перемещения.
Исключительные ситуации
Сценарий 1: Нехватка товара (Shortage)
Логист не может найти все в требуемом количестве:
Operation Item: Chairs
RequestedQty: 10
ReservedQty: 10 (на складе)
FulfilledQty: 10 ✓
ShortageQty: 0
Operation Item: Projector
RequestedQty: 1
ReservedQty: 0
FulfilledQty: 0
ShortageQty: 1 ← НЕ МОЖЕМ НАЙТИ
Operation Status: "exception" (требуется решение по замене или отмене)
Comment: "Projector not available at any supplier. Recommend renting alternative model."Что дальше?
- Логист уведомляет менеджер проекта (через систему уведомлений)
- Менеджер решает: взять альтернативу, отложить, уменьшить количество
- Создается новая операция или текущая корректируется
- Если проблема не решена, операция может быть отменена (статус cancelled)
Сценарий 2: Превышение бюджета
Logistics Operation Budget:
PlannedCost: 10000
ActualCost: 15000 (rental оказался дороже)
Overage: 5000
Operation Status: "under_review" (сумма требует финансового решения)
Comment: "Actual rental cost higher due to premium model required. Awaiting approval."Менеджер видит перерасход:
- Может одобрить (бюджет есть, обновляется ActualCost в финансах)
- Может потребовать изменения (взять более дешевый вариант, создать новую операцию)
- Может скорректировать плановый бюджет проекта
Сценарий 3: Задержка доставки
Logistics Operation:
PlannedArrival: 2026-04-25 10:00
ActualDeparture: 2026-04-24 14:00
Status: "in_transit"
Delay Notification:
"Delivery delayed. New estimated arrival: 2026-04-25 16:00"Менеджер может:
- Отложить событие
- Найти альтернативу (временный проектор)
- Скорректировать расписание
Ключевые правила логистического контура
1. Логистика решает, как получить товар
Система НЕ требует одобрения менеджера для каждого решения. Логист выбирает между:
- Купить (если есть бюджет и есть на рынке)
- Арендовать (если временно нужна)
- Со склада (если есть и свободно)
- С другого проекта (если совместимо по срокам)
2. Все одобренные затраты на логистику автоматически переходят в бюджет
Менеджер не вводит финансовые данные дважды. После finance_status=approved плановая сумма операции создается/обновляется в finance как source_type=logistic_operation. После завершения операции фактическая сумма также синхронизируется системой.
3. Операция может быть отменена на любом этапе до закрытия
Если требования изменились или товар больше не нужен, статус меняется на cancelled. После closed операция считается закрытой и не редактируется.
4. Каждый шаг оставляет след в истории
- Кто создал операцию
- Кто изменил статус
- Когда произошла смена статуса
- Какие комментарии были добавлены
Это помогает в отчетности и анализе задержек/проблем.
5. Операция может быть "Exception"
Если что-то пошло не по плану и требуется вмешательство.
Резюме: путь товара через логистику
NEED IDENTIFIED
↓
REQUEST CREATED (request_created)
↓
LOGISTICS REVIEW (under_review)
→ Логист решает: купить, арендовать, со склада или перевезти
↓
AWAITING SUPPLY (awaiting_supply)
→ Если нужна закупка: ждем поставку
↓
SOURCED (sourced)
→ Товар получен и находится на складе/в офисе
↓
PREPARING SHIPMENT (preparing_shipment)
→ Упаковываем и готовим к доставке
↓
IN TRANSIT (in_transit)
→ Товар в пути
↓
DELIVERED (delivered)
→ Товар на объекте, но еще не принят
↓
ACCEPTED (accepted)
→ Менеджер проверил и принял
↓
COMPLETED (completed)
→ Доставка выполнена, факт и затраты можно зафиксировать
↓
CLOSED (closed)
→ Операция закрыта, оборудование перемещено на новую локацию, задача завершенаНа выходе менеджер получает:
- Товар на объекте
- Затраты в бюджете
- Документирование всего процесса
ТЗ для дизайнера: чистый контур логиста
Общая информация
Раздел «Логистика» — это рабочее место логиста. Здесь логист сам создает, планирует, ведет и закрывает логистические операции по перемещению оборудования между локациями.
Заявка менеджера — отдельный upstream-сценарий. Для менеджера это только запрос на оборудование: что нужно, сколько, к какому проекту и к какому сроку. Менеджер не выбирает водителя, ТК, машину, маршрут забора с разных локаций и не ведет статусы логистической операции. Весь операционный движ происходит у логиста.
Одна логистическая операция всегда означает одно перемещение:
- одна локация отправления;
- одна локация назначения;
- один набор оборудования;
- один перевозчик или ответственный исполнитель;
- одна связанная задача.
Если оборудование нужно собрать с нескольких локаций, система создает несколько независимых логистических операций.
Backend-источники для чистого контура логиста:
- оборудование:
GET /api/v1/equipment; - остатки по карточке оборудования:
GET /api/v1/equipment/:id/stock; - локации:
GET /api/v1/locations; - основной склад:
GET /api/v1/locations?is_main_warehouse=true; - список операций:
GET /api/v1/logistics; - создание операции:
POST /api/v1/logistics; - карточка операции:
GET /api/v1/logistics/:id; - редактирование операции:
PATCH /api/v1/logistics/:id; - подтверждение маршрута и создание задачи:
POST /api/v1/logistics/:id/confirm; - закрытие операции:
POST /api/v1/logistics/:id/close.
Важное правило резервов
Оборудование, находящееся в любой незакрытой логистической операции, считается зарезервированным.
Незакрытые статусы:
request_created;under_review;awaiting_supply;sourced;preparing_shipment;in_transit;delivered;accepted;completed.
Резерв снимается только при:
closed;cancelled;exception.
Список логистических операций
Экран списка показывает операции из GET /api/v1/logistics.
Фильтры:
project_id;from_location_id;to_location_id;status;fulfillment_type.
В строке операции показывать:
id;name;status;project_id, если есть;from_location_id;to_location_id;carrier_type;carrier;driver_idилиdriver_name;vehicle_number;planned_departure;planned_arrival;actual_departure;actual_arrival;- признак задачи
task_id; - признак подтверждения
confirmed_at; - дату обновления
updated_at.
Основные действия:
- Добавить операцию;
- открыть карточку операции;
- редактировать операцию;
- подтвердить маршрут;
- перевести статус;
- закрыть операцию;
- отменить операцию.
Сценарий 1. Создание одной операции, если оборудование есть на основном складе
Шаг 1. Логист нажимает «Добавить операцию»
Открывается мастер создания операции.
Первый экран — выбор оборудования.
Поля выбора:
- поиск по каталогу;
equipment_id;name;category;unit;requested_qty;comment, необязательно.
Кнопка «Далее» активна, если выбрана хотя бы одна позиция и у каждой позиции requested_qty > 0.
Шаг 2. Система проверяет фактическое наличие
Для каждой позиции система показывает остатки:
- локация хранения;
- всего на локации;
- зарезервировано в незакрытых логистических операциях;
- доступно сейчас;
- хватает / не хватает;
- единица измерения.
Если всё нужное количество есть на основном складе (is_main_warehouse=true), система предлагает создать одну операцию.
Шаг 3. Логист заполняет параметры операции
Поля операции:
name— название операции;project_id— проект, необязательно;from_location_id— основной склад;to_location_id— локация назначения;planned_departure— плановая дата/время отправки;planned_arrival— плановая дата/время доставки;fulfillment_type— способ обеспечения:internal_transfer,purchase,rental;carrier_type— тип перевозчика:own_driver,transport_company,courier,supplier_delivery,pickup,other;carrier— название ТК/курьерской службы/поставщика;responsible_id— логист, который ведет операцию;driver_id— собственный водитель из пользователей;driver_name— водитель текстом, если его нет в пользователях;vehicle_number— номер машины;tracking_number— трек-номер;pickup_address— адрес забора;delivery_address— адрес доставки;planned_cost— плановая стоимость логистики;notes— комментарий.
Зависимости полей:
- если
carrier_type=own_driver, нуженdriver_idилиdriver_name; - если
carrier_type=transport_company,courierилиsupplier_delivery, нужныcarrierиresponsible_id; - если выбран
project_id, операция отображается в проектном контуре, но управление все равно остается у логиста; from_location_idиto_location_idне могут совпадать.
Шаг 4. Создание операции
После сохранения создается логистическая операция со статусом request_created.
На этом этапе:
- оборудование уже считается зарезервированным в расчетах доступности;
- физическая локация оборудования еще не меняется;
- задача еще не создается.
Шаг 5. Подтверждение маршрута
Когда логист готов отдать операцию в исполнение, он нажимает «Подтвердить маршрут».
Backend:
- переводит операцию в
preparing_shipment; - заполняет
confirmed_by; - заполняет
confirmed_at; - создает связанную задачу.
Кому ставится задача:
- при
carrier_type=own_driverзадача ставится водителю изdriver_id; - при
transport_company,courier,supplier_deliveryзадача ставится логисту изresponsible_id; - если операция без проекта, задача не создается, потому текущая модель задач требует
project_id.
Шаг 6. Исполнение операции
До closed логист может редактировать:
- дату и время;
- перевозчика;
- ответственного;
- водителя;
- номер машины;
- трек-номер;
- адреса;
- комментарии;
- плановую и фактическую стоимость;
- фактическое время отправки и прибытия;
- статус.
Статусы операции:
request_created— создана;under_review— на рассмотрении;awaiting_supply— ожидает обеспечения;sourced— источник найден, позиции зарезервированы;preparing_shipment— маршрут подтвержден, задача создана;in_transit— в пути;delivered— доставлено;accepted— принято;completed— доставка выполнена, факт заполнен;closed— операция закрыта, оборудование перемещено;cancelled— отменена;exception— проблема.
Шаг 7. Закрытие операции
Кнопка «Закрыть операцию» доступна только из completed.
При закрытии backend:
- ставит статус
closed; - переносит оборудование в
to_location_id; - если локация назначения склад или офис, очищает
project_idу оборудования и ставит статусavailable; - если локация назначения объект/клиентская площадка и есть проект, ставит оборудованию
project_idи статусin_use; - завершает связанную задачу;
- пишет событие в историю операции.
После closed операция не редактируется.
Сценарий 2. На основном складе оборудования не хватает
Если на основном складе не хватает части оборудования, логист видит экран комплектации.
Для каждой позиции показывать:
- что требуется;
- сколько требуется;
- сколько доступно на основном складе;
- сколько не хватает;
- на каких других локациях есть доступный остаток;
- сколько доступно на каждой локации с учетом незакрытых операций.
Логист указывает, сколько забрать с каждой локации.
Пример:
Нужно 10 единиц.
Доступно:
- Склад N2 — 4 шт.;
- Объект А — 3 шт.;
- Объект Б — 3 шт.
Логист выбирает:
- забрать 4 шт. со Склада N2;
- забрать 3 шт. с Объекта А;
- забрать 3 шт. с Объекта Б.
Перед продолжением система показывает предупреждение:
Для комплектации потребуется создать несколько логистических операций. Каждое перемещение будет оформлено отдельной операцией и отдельной задачей.
После подтверждения создаются отдельные операции:
- Склад N2 → основной склад.
- Объект А → основной склад.
- Объект Б → основной склад.
Для каждой операции отдельно заполняются:
planned_departure;planned_arrival;carrier_type;carrier;responsible_id;driver_idилиdriver_name;vehicle_number;tracking_number;pickup_address;delivery_address;planned_cost;notes.
Каждая операция далее проходит стандартный цикл: подтверждение маршрута, задача, статусы исполнения, completed, затем closed.
Отдельный upstream-сценарий: заявка менеджера
Этот сценарий не является чистым контуром логиста и должен проектироваться отдельно.
Для менеджера интерфейс проще:
- выбрать проект;
- выбрать оборудование;
- указать количество;
- указать желаемую дату/срок;
- оставить комментарий;
- отправить заявку.
Менеджер не выбирает:
- откуда забирать оборудование;
- сколько забирать с каждой локации;
- перевозчика;
- водителя;
- номер машины;
- маршрут;
- статусы логистической операции.
После отправки заявка попадает логисту как входящая потребность. Логист уже в своем контуре выполняет подбор источников, разбиение на операции, назначение задач и закрытие перемещений.
Бизнес-правила интерфейса логиста
- Нельзя создать операцию без оборудования.
- Нельзя указать количество больше доступного без выбора другого источника, закупки или аренды.
- Оборудование в незакрытых операциях всегда показывается как резерв.
from_location_idиto_location_idне могут совпадать.- При
carrier_type=own_driverтребуетсяdriver_idилиdriver_name. - При
transport_company,courier,supplier_deliveryтребуетсяcarrierиresponsible_id. - Фактическое местонахождение оборудования меняется только при
closed. - Закрытую операцию нельзя редактировать.
- В карточке операции нужно показывать историю событий: создание, подтверждение, обновления, смены статусов, закрытие.
- Если оборудование собирается с нескольких локаций, создаются несколько независимых операций.
Единый список оборудования проекта (без дублей)
Оборудование проекта как бизнес-потребность хранится в project_equipment_requirements. Открытая (черновая) заявка project_equipment остается удобной точкой доливки и отправки логистам, но больше не является единственным источником количества “проекту нужно N”:
- Открытая (черновая) заявка — родительская заявка проекта в статусе
request_created. В неё мерджатся добавления и импорт: повторequipment_idобновляет кол-во/дни/цену, новые позиции добавляются. Дублей не возникает. - Если открытого черновика нет (первое добавление, либо предыдущий черновик уже отправлен/спланирован) — создаётся новый черновик. Старая заявка при этом никуда не девается: она уже в работе (
under_review/sourced/…) и её позиции по‑прежнему относятся к проекту.
Поэтому количество не исчезает из проекта после submit, cancel или archive.
GET /projects/:id/equipment-statusчитаетproject_equipment_requirementsи возвращаетrequested_qtyплюс явный aliasproject_requested_qty.GET /projects/:id/equipment-requests?include_items=trueнужен для карточек заявок и истории исполнения; у item-ов также может бытьproject_requested_qty, но логистическийrequested_qtyостается количеством конкретной заявки.
Что считать «оборудованием проекта» для экрана: authoritative-строки берите из GET /projects/:id/equipment-status; активные заявки проекта добавляйте как контекст статуса, действий и логистической истории. Заявки в работе (under_review, in_progress, in_transit, completed) и открытый черновик (created) показываются как исполнение потребности, но не должны быть единственным источником required quantity.
Удаление позиций заявки: позиции можно удалять из родительской черновой заявки (status=created, logistics_status=request_created, finance_status=draft, procurement_status=not_started, включая архивный черновик после cancel без операций) либо из отмененной заявки (cancelled/cancellation_requested), когда по ней нет активных дочерних операций:
DELETE /api/v1/equipment-requests/:id/items/:item_id— удалить одну сохранённую позицию;POST /api/v1/equipment-requests/:id/items/deleteс телом{ "item_ids": [1, 2] }— удалить несколько позиций.
Сохранение оборудования в проекте: строку каталожного оборудования сохраняйте через PUT /api/v1/projects/:id/equipment-requirements/:equipment_id с телом { "required_qty": 100 }. Endpoint создаёт или обновляет project_equipment_requirements.required_qty и не создаёт логистическую заявку. Заявка остаётся отдельным workflow для нехватки, закупки, аренды или перемещения.
Удаление оборудования из проекта: если нужно убрать строку из списка оборудования проекта, используйте DELETE /api/v1/projects/:id/equipment-requirements/:equipment_id. Этот endpoint удаляет проектную потребность (project_equipment_requirements) и дополнительно чистит совпадающие позиции только из безопасно редактируемых черновых/отменённых заявок. Активная логистическая история и уже спланированные операции не отменяются автоматически.
Если заявка уже отправлена или спланирована, её позиции считаются частью логистической/финансовой истории. Для изменения состава сначала отмените заявку через POST /api/v1/equipment-requests/:id/cancel и дождитесь, пока по ней не останется активных дочерних операций. Отмена заявки менеджером (cancel, ниже) сама по себе не удаляет позиции и не обнуляет проектную потребность — она архивирует заявку или ставит ожидание отмены операций; удаление позиций выполняется отдельным endpoint.
Импорт и каталог: при импорте позиция сопоставляется с каталогом по нормализованному имени (регистр + схлопывание пробелов). Существующая позиция переиспользуется, новая создаётся только если совпадения нет — повторный импорт того же оборудования каталог не плодит.
Доступ к заявкам и уведомление логистам
- Менеджер видит заявки в рамках проекта:
GET /projects/:id/equipment-requests(доступ к логистическому разделу проекта). Создают, правят, отменяют и импортируют заявки проекта его команда и те, кто его ведёт, плюс склад и логистика (см. роли заявок). - Логист видит все заявки в системе:
GET /equipment-requests(правоlogistics.manage, которое выдаётся логистам). Фильтры:project_id,parent_request_id,include_items. - Отправка заявки в логистику (
POST /equipment-requests/:id/submit, заявка →under_review) создаёт задачу логисту проекта и тем самым уведомляет его (создание задачи рассылает уведомление назначенному). Исполнитель определяется как и для логистических операций (логист проекта). Если логист по проекту не определён — задача не создаётся. Сбой уведомления не блокирует отправку заявки (best-effort).
Задача создаётся на этапе submit, а не на каждое добавление оборудования в черновик — иначе мердж-доливки спамили бы логиста. Submit = момент, когда заявка реально уходит в работу логистам.
- Проект заявки для склада. Заявка (
GET /equipment-requests,/equipment-requests/:id,/projects/:id/equipment-requests) несётproject_name,project_location_idиproject_location_name: складу карточки проектов по матрице закрыты, а ему нужно видеть, для кого и куда везти. Мастер операций по заявке ставит пунктом назначения площадку проекта, а не основной склад. - Финансовое решение по перевозке. Операция с
planned_costждётfinance/approve(только финансы — затраты «все» — и глобальный администратор); финансы видят перевозки проекта (GET /logistics?project_id=) по разделу «Финансы» и решают в карточке проекта: «ТЗ и смета» → «Смета и бюджет» → «Перевозки ждут решения по сумме». Одобрение заводит статью «Логистика».
Отмена заявки менеджером (cancel)
Менеджер может отменить логистику по заявке — например, маршрут спланировали неправильно и нужно переделать:
POST /api/v1/equipment-requests/{id}/cancelВажно: cancel — это не удаление позиций. Если активных дочерних операций нет, заявка архивируется и пропадает из активного списка проекта. Если операции уже есть, заявка переходит в cancellation_requested и ждёт, пока логист отменит активные дочерние операции.
Логика зависит от того, успел ли логист создать операции по заявке:
- Операций ещё нет → заявка сразу архивируется.
- Операции уже созданы → заявка переходит в промежуточный статус
cancellation_requestedи ждёт логиста. Логист отменяет операции в одностороннем порядке (PATCH /api/v1/logistics/:idсоstatus=cancelled, без отдельного согласования); отмена операции освобождает её резерв. Когда отменена последняя активная операция — заявка автоматически архивируется.
Так логист одним действием (отменил единственную операцию) завершает отмену заявки.
Детали:
- Отменять можно только родительскую заявку (
parent_request_id IS NULL); попытка отменить дочернюю операцию через этот эндпоинт —400. - Нельзя отменить уже завершённую/отменённую/отклонённую заявку (
completed/cancelled/rejected) —409. (Вcancelled/rejectedзаявка попадает через workflow-отклонение; менеджерскийcancelархивирует заявку.) - Повторный
cancelпо заявке вcancellation_requestedидемпотентен: если операции уже отменены, заявка добивается до архива. - Финализация отмены реализована наблюдателем: логистика после отмены операции с
parent_request_idдёргаетFinalizeCancellationIfReadyу заявки. - Доступ — как к логистическому разделу проекта (участник проекта), либо
logistics.manage/admin для standalone.
Активной считается дочерняя операция со статусом, отличным от
cancelled. Если по заявке есть уже доставленные/закрытые операции, автоархивация не сработает (оборудование фактически перемещено) — это ожидаемо: такие операции не отменяются.
Что видит фронт после отмены
После архивации заявка скрыта из активного списка проекта. GET /projects/:id/equipment-status продолжает возвращать проектную потребность из project_equipment_requirements и пересчитанный shortage. Если UI запрашивает историю с include_archived=true, позиции остаются в архивной заявке до явного удаления через DELETE /equipment-requests/:id/items/:item_id или POST /equipment-requests/:id/items/delete; backend разрешает это только когда нет активных дочерних операций. Отменённые дочерние операции остаются в истории (status=cancelled), но не считаются активными и не резервируют оборудование.
Где живёт оборудование проекта и почему не исчезает
Это сводный контур «от и до» для фронта: что такое «оборудование проекта», как оно проходит по статусам и почему его нельзя «потерять» с экрана проекта. Раздел отвечает на две частые проблемы: «оборудование пропало после отправки/планирования» и «оборудование пропало после отмены».
Модель данных (одно предложение)
Проектная потребность — это project_equipment_requirements(project_id, equipment_id, required_qty). Заявка на оборудование и логистическая операция — это одна и та же таблица logistic_operations; позиции заявки/операции — logistic_operation_items. Родительская заявка проекта: request_kind=project_equipment, parent_request_id IS NULL. Дочерняя операция логиста: request_kind=planned_operation, parent_request_id=<id заявки>. Статус заявки не меняет required_qty проекта — поэтому потребность не пропадает из агрегата после отмены или архивации заявки.
Где брать список «оборудование проекта»
GET /api/v1/projects/:id/equipment-statusВозвращает авторитетную обеспеченность проекта по project_equipment_requirements: requested_qty, project_requested_qty, available на точке, основной склад, reserved, in_transit, closed, shortage и ids связанных заявок/операций. Для карточек заявок и истории используйте:
GET /api/v1/projects/:id/equipment-requests?include_items=trueЭтот endpoint возвращает активные родительские заявки проекта с позициями. Архивную историю запрашивайте отдельно через include_archived=true.
- Это НЕ «только открытый черновик». Открытый черновик (
status=created) — лишь заявка, в которую доливаются новые позиции; кроме него у проекта могут быть заявки в работе (under_review,in_progress,in_transit,completed) — их позиции тоже относятся к проекту. - Дедуп: одно и то же
equipment_idв рамках одной заявки не дублируется (мердж при добавлении/импорте). Если показываете объединение нескольких заявок — группируйте поequipment_idна фронте.
Жизненный цикл заявки и где на каждом шаге оборудование
| Шаг | Эндпоинт | status заявки | Что с оборудованием на экране проекта |
|---|---|---|---|
| Менеджер добавляет/импортирует | POST /projects/:id/equipment-requests, импорт | created (открытый черновик; доливка в него) | Видно |
| Менеджер удаляет позицию из черновика/отмененной заявки без активных операций | DELETE /equipment-requests/:id/items/:item_id, POST /equipment-requests/:id/items/delete | created, cancelled или cancellation_requested без активных операций | Удаляется строка заявки/истории; equipment-status сохраняет project required qty |
| Менеджер отправляет в логистику | POST /equipment-requests/:id/submit | under_review | Видно (заявка ушла из created, но позиции на месте) |
| Финансы согласуют сумму заявки (опционально) | POST /equipment-requests/:id/finance/approve | under_review | Видно |
| Логист планирует операции | POST /equipment-requests/:id/operation-plan/confirm | in_progress (sourced) | Видно (создаются дочерние операции, позиции заявки на месте) |
| Логист ведёт и закрывает операции | PATCH/POST /logistics/:id/... | in_transit → completed | Видно |
| Менеджер отменяет (cancel) | POST /equipment-requests/:id/cancel | архивируется сразу или cancellation_requested до отмены операций | Заявка скрывается после архивации; equipment-status продолжает показывать потребность и shortage |
| Финансы/закупки отклонили (reject) | POST .../finance/reject, .../procurement/reject | rejected | Заявка скрыта как отклонённая; проектная потребность не сбрасывается |
Ключ: submit и планирование оборудование НЕ убирают. Активная карточка заявки может уйти с экрана при archive/cancel/rejected, но проектная строка в equipment-status остается, пока required quantity хранится в project_equipment_requirements.
Почему оборудование больше не пропадает
После submit/планирования. Заявка меняет
status, но её позиции остаются в строке заявки. Раньше казалось, что оборудование «пропало», если фронт показывал только открытый черновик (created). Правильно — строить проектные строки изGET /projects/:id/equipment-status, а заявки изGET /projects/:id/equipment-requests?include_items=trueиспользовать как контекст исполнения.После отмены.
cancelархивирует заявку, если активных операций нет, либо переводит ее вcancellation_requestedдо отмены операций логистом (см. «Отмена заявки менеджером»). После архивации заявка скрыта из активного списка проекта, ноequipment-statusчитает required quantity изproject_equipment_requirements; если нужно убрать строки из архивной истории, удалите позиции отдельным endpoint после завершения отмены.
Краткая шпаргалка по эндпоинтам
| Действие | Метод и путь | Кто | Право/доступ |
|---|---|---|---|
| Статус оборудования проекта | GET /projects/:id/equipment-status | менеджер/логист | логистический раздел проекта |
| Заявки и история исполнения | GET /projects/:id/equipment-requests?include_items=true | менеджер/логист | логистический раздел проекта |
| Сохранить/обновить оборудование проекта | PUT /projects/:id/equipment-requirements/:equipment_id | менеджер | логистический раздел проекта |
| Добавить оборудование | POST /projects/:id/equipment-requests | менеджер | логистический раздел проекта |
| Удалить оборудование из проекта | DELETE /projects/:id/equipment-requirements/:equipment_id | менеджер | логистический раздел проекта |
| Удалить одну позицию из черновика/отмененной заявки | DELETE /equipment-requests/:id/items/:item_id | менеджер | логистический раздел проекта |
| Удалить несколько позиций из черновика/отмененной заявки | POST /equipment-requests/:id/items/delete | менеджер | логистический раздел проекта |
| Отправить заявку в логистику | POST /equipment-requests/:id/submit | менеджер | логистический раздел проекта |
| Все заявки (рабочий стол логиста) | GET /equipment-requests | логист | logistics.manage |
| Спланировать операции | POST /equipment-requests/:id/operation-plan/{preview,confirm} | логист | logistics.manage |
| Создать операцию напрямую | POST /logistics | логист | логистический раздел проекта |
| Отменить операцию (в одностороннем порядке) | PATCH /logistics/:id {status:"cancelled"} | логист | logist/pm/admin |
| Отменить заявку | POST /equipment-requests/:id/cancel | менеджер | логистический раздел проекта |
| Согласование суммы операции | POST /logistics/:id/finance/{approve,reject} | финансист | finance.manage |
Напоминание про финансы (см. начало документа): создание/планирование операций финансами не блокируется. Сумма по операции (
planned_cost) уводит её вfinance_status=pendingи ставит задачу финансисту; approve нужен только чтобы двигать операцию в исполнение.
Заявка менеджера до конца: что делает система (2026-09-29)
Сквозной прогон «менеджер → логист → финансы → логист» показал, где заявка терялась. Теперь так:
Что в заявке. Кнопка «Создать заявку» в карточке проекта заказывает, сколько довезти на площадку: потребность минус то, что уже на площадке, и то, что уже едет или заказано. Склад не вычитается — со склада тоже нужно везти; откуда брать (склад, закупка, аренда), решает логист. Баннер — «Нужно довезти на площадку». Бронь позиции при создании заявки не считает оборудование, лежащее на площадке проекта.
Мастер логиста. Площадка назначения не предлагается источником. У закупки и аренды нет площадки-источника: маршрут подтверждается без «Откуда» (в карточке — «Аренда · поставщик»). Кто подтверждает маршрут без назначенного ответственного, тот им и становится — иначе внешнего перевозчика не подтвердить.
Деньги. Цена закупки/аренды из мастера попадает в сумму операции (см. «Автоплан заявки»). При закрытии операции логист вносит «Фактические расходы, ₽» (по умолчанию — план); факт уходит в статью затрат проекта. Задачу «Согласовать сумму перевозки» получает тот, кто вправе согласовать: запись затрат «все» (FIN_DIR, затем OPS, затем собственник; сначала из команды проекта), а не бухгалтер (у FIN_ASSIST затраты только на чтение). Тексты задач — с названиями площадок, датами по часовому поясу компании и подписью способа перевозки; срок задачи — день отправки по часовому поясу компании.
Заявка следует за перевозками. Операции созданы — задача логиста «Новая заявка на оборудование» закрывается сама (задача привязана к заявке через logistics_task_id). Хоть одна перевозка в пути — заявка «в пути», все закрыты — «завершена». Все активные перевозки согласованы финансами — у заявки finance_status=approved. Сохранённые до исправления заявки выравнивает миграция 0152. Отмена операции закрывает её задачи.
Статьи затрат. Сумма перевозки уходит в бюджет двумя статьями: закупка/аренда оборудования (цена × количество позиций закупки и аренды) — «Оборудование» (source_type=logistic_operation_supply, в интерфейсе источник «Закупка / аренда», название «Аренда: …» / «Закупка: …»), остальное — доставка — «Логистика» (logistic_operation). Факт при закрытии делится так же: закупка/аренда забирает свою цену, доставке — остаток. Уже сохранённые статьи разделила миграция 0153. Отменённая перевозка не ждёт решения финансов: в бюджете её нет в «Перевозки ждут решения», сервер её не согласует.
Доступность в проекте. Закрытый внутренний трансфер лежит на площадке и считается один раз (available_at_target_qty); closed_qty теперь — только привезённое закупкой и арендой (своей единицы в каталоге у него нет). Закупка и аренда в пути считаются по заказанному количеству. Интерфейс подписывает это «закупка/аренда».
Позиция проекта. PUT /api/v1/projects/:id/equipment-requirements/:equipment_id принимает rental_days и unit_price (необязательные; не переданы — остаются прежними): дни и цена «в проекте» больше не теряются после перезагрузки. Создание заявки не затирает цену «в проекте».
Справочники логистики: что сервер не даст сделать (2026-09-29)
- Оборудование. Правка карточки каталога сохраняет проект позиции (его ставит логистика) и прежнего поставщика, даже отключённого: позиция «В резерве»/«В работе» больше не падает на
equipment_project_id_required_for_status. Вручную статус — только «В наличии» или «Сервис». Удаление оборудования в незакрытых операциях или на действующем проекте запрещено; интерфейс узнаёт это сухим прогоном (DELETE …?dry_run=true) и объясняет по-русски. - Локации. В архив не отправить то, что используется сейчас: оборудование на месте, незакрытые операции и поездки. Прошлые операции и поездки архиву не мешают. Выключение «в работе» в форме проверяется так же. Основной склад не архивируется и не меняет тип «Склад».
- Поставщики. Поставщика с действующим оборудованием переключатель «активен» не отключит — только «Удалить» с подтверждением.
- Мастер. Новая локация из мастера сохраняется в справочник (раньше — только в мастере, и операция не подтверждалась). Оборудование в пути, на сервисе и сломанное — не в наличии. Закупка и аренда без заявки не бронируют склад: у позиций источник операции. Когда трансфером не обеспечить, мастер подсказывает «Аренда» или «Закупка» (склад автоплан возьмёт сам).
- Автоплан. Сумма доставки одна на маршрут — достаётся первой операции; остальные получают только цену закупки/аренды. Раньше доставка попадала в каждую операцию.
- Смешанный план. В режиме «Аренда»/«Закупка» по заявке мастер сам показывает маршрут со склада на то, что есть в наличии, и отдельный маршрут на недостающее — у каждого свои перевозчик, даты и сумма (
routes[]). Раньше склад и прокат ехали одним маршрутом с одним перевозчиком. - Закрытие. Фактические расходы — неотрицательное число; иначе закрыть нельзя.