Skip to content

Логистический контур: как работает подбор оборудования ​

Это описание того, как в системе работает процесс получения оборудования для проекта — от момента, когда менеджер понимает, что что-то нужно, до момента, когда оборудование доставлено и принято.


Общая схема ​

В текущей версии 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: Определение потребностей ​

Как начинается логистический процесс ​

Обычный сценарий:

  1. Менеджер проекта смотрит на требования из ТЗ.
  2. Понимает, какое оборудование/материалы нужны.
  3. Составляет список в системе (или вручную).
  4. Проверяет наличие в системе.

Пример:

Проект: 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_transfer
  • type=purchase → fulfillment_type=purchase
  • type=rental → fulfillment_type=rental
  • type=unspecified → fulfillment_type=NULL

Минимальный payload для создания:

json
{
  "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 сам строит план:

http
POST /api/v1/equipment-requests/:id/auto-plan/preview
POST /api/v1/equipment-requests/:id/auto-plan/confirm

preview ничего не пишет в БД. confirm создает дочерние операции через основной /logistics service, поэтому работают задачи, аудит, финансовый gate и синхронизация затрат.

Порядок закрытия потребности:

  1. Засчитывается оборудование, которое уже есть на точке доставки — кроме заявок «довезти» из карточки проекта: их количество уже за вычетом площадки (у позиции project_requested_qty больше requested_qty), и лежащее там второй раз не вычитается.
  2. Затем берется основной склад СКЛАД КАЗАНЬ.
  3. Затем другие склады и проектные точки.
  4. Если внутреннего наличия не хватает, draft purchase создается только при allow_purchase=true, draft rental — только при allow_rental=true.
  5. Если source locations разные, backend создает разные операции: например А -> Б и В -> Б.

Пример payload:

json
{
  "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 — водитель, сумма неотрицательная.

json
"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 означает, что часть потребности не закрыта и нужно разрешить закупку/аренду или изменить количество.

Взять в работу и архивировать ​

Логист может назначить ответственного:

http
POST /api/v1/logistics/:id/take
json
{ "responsible_id": 44 }

Если responsible_id не передан, используется текущий пользователь.

Для родительских заявок:

http
POST /api/v1/equipment-requests/:id/archive
POST /api/v1/equipment-requests/:id/reject
json
{ "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 — операцию можно создать черновиком, без маршрута и без суммы:

json
{ "name": "Доставка на объект" }

Полный payload (все поля опциональны, кроме name):

ПолеТипКогда нужно
namestringобязательно
project_idintесли операция привязана к проекту (включает финансовый и задачный контур)
responsible_idintлогист-исполнитель; на него вешается логистическая задача
from_location_id / to_location_idintоткуда/куда; не могут совпадать
fulfillment_typeenuminternal_transfer / purchase / rental
carrier_typeenumown_driver / courier / transport_company / supplier_delivery / pickup / other
carrierstringпри courier/transport_company/supplier_delivery
driver_id / driver_nameint / stringпри carrier_type=own_driver нужен один из них
vehicle_number, tracking_numberstringпо необходимости
pickup_address, delivery_addressstringпо необходимости
planned_departure, planned_arrivalRFC3339плановые даты; arrival ≥ departure
planned_costnumberплановая стоимость логистики (см. ниже про задачу финансисту)
items[]array{ equipment_id, requested_qty, rental_days?, source_type?, comment? }
notesstringкомментарий

Что произойдёт с финансами при создании ​

Поведение зависит только от planned_cost и project_id в момент создания:

Сценарий при созданииfinance_status операцииАвто-задача
есть project_id и planned_costpendingзадача финансисту проекта: «Финансы: обработать логистическую операцию»
есть project_id, нет planned_costamount_missingлогистическая задача на ведение операции
нет project_idamount_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

Ответ:

json
{
  "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 (логист уточняет источник, сроки и стоимость)

Логист (или логистический менеджер) получает заявку и начинает анализировать:

  1. Что на складе — все ОК:

    • Стулья (10 шт) — берем
    • Столы (3 шт) — берем
    • Кабельная система (1 компл) — берем
  2. Что нужно найти (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, когда источник найден и позиции можно резервировать:

  1. Заказано (Comment: "Ordered from Supplier X, ETA: 2 дня")
  2. Получено на склад (Comment: "Received, ready for shipment", FulfilledQty увеличивается)
  3. Готовится к доставке (Comment: "Packing for shipment")
  4. Отправлено (когда статус меняется на 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, когда источник готов:

  1. Ждем освобождения (Comment: "Waiting for Project B to finish by Friday")
  2. Забрали и готовим (Comment: "Picked up from Project B warehouse")
  3. Отправлено (когда статус меняется на 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 шт. с Объекта Б.

Перед продолжением система показывает предупреждение:

Для комплектации потребуется создать несколько логистических операций. Каждое перемещение будет оформлено отдельной операцией и отдельной задачей.

После подтверждения создаются отдельные операции:

  1. Склад N2 → основной склад.
  2. Объект А → основной склад.
  3. Объект Б → основной склад.

Для каждой операции отдельно заполняются:

  • 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-сценарий: заявка менеджера ​

Этот сценарий не является чистым контуром логиста и должен проектироваться отдельно.

Для менеджера интерфейс проще:

  • выбрать проект;
  • выбрать оборудование;
  • указать количество;
  • указать желаемую дату/срок;
  • оставить комментарий;
  • отправить заявку.

Менеджер не выбирает:

  • откуда забирать оборудование;
  • сколько забирать с каждой локации;
  • перевозчика;
  • водителя;
  • номер машины;
  • маршрут;
  • статусы логистической операции.

После отправки заявка попадает логисту как входящая потребность. Логист уже в своем контуре выполняет подбор источников, разбиение на операции, назначение задач и закрытие перемещений.

Бизнес-правила интерфейса логиста ​

  1. Нельзя создать операцию без оборудования.
  2. Нельзя указать количество больше доступного без выбора другого источника, закупки или аренды.
  3. Оборудование в незакрытых операциях всегда показывается как резерв.
  4. from_location_id и to_location_id не могут совпадать.
  5. При carrier_type=own_driver требуется driver_id или driver_name.
  6. При transport_company, courier, supplier_delivery требуется carrier и responsible_id.
  7. Фактическое местонахождение оборудования меняется только при closed.
  8. Закрытую операцию нельзя редактировать.
  9. В карточке операции нужно показывать историю событий: создание, подтверждение, обновления, смены статусов, закрытие.
  10. Если оборудование собирается с нескольких локаций, создаются несколько независимых операций.

Единый список оборудования проекта (без дублей) ​

Оборудование проекта как бизнес-потребность хранится в 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 плюс явный alias project_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 связанных заявок/операций. Для карточек заявок и истории используйте:

http
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/deletecreated, cancelled или cancellation_requested без активных операцийУдаляется строка заявки/истории; equipment-status сохраняет project required qty
Менеджер отправляет в логистикуPOST /equipment-requests/:id/submitunder_reviewВидно (заявка ушла из created, но позиции на месте)
Финансы согласуют сумму заявки (опционально)POST /equipment-requests/:id/finance/approveunder_reviewВидно
Логист планирует операцииPOST /equipment-requests/:id/operation-plan/confirmin_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/rejectrejectedЗаявка скрыта как отклонённая; проектная потребность не сбрасывается

Ключ: submit и планирование оборудование НЕ убирают. Активная карточка заявки может уйти с экрана при archive/cancel/rejected, но проектная строка в equipment-status остается, пока required quantity хранится в project_equipment_requirements.

Почему оборудование больше не пропадает ​

  1. После submit/планирования. Заявка меняет status, но её позиции остаются в строке заявки. Раньше казалось, что оборудование «пропало», если фронт показывал только открытый черновик (created). Правильно — строить проектные строки из GET /projects/:id/equipment-status, а заявки из GET /projects/:id/equipment-requests?include_items=true использовать как контекст исполнения.

  2. После отмены. 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[]). Раньше склад и прокат ехали одним маршрутом с одним перевозчиком.
  • Закрытие. Фактические расходы — неотрицательное число; иначе закрыть нельзя.

Загружаем документацию…