Skip to content

Командировки ​

Модуль trips оформляет командировки сотрудников и связывает их с проектом, маршрутом, датами и финансовым контуром.

Роли ​

В карточке командировки есть три участника:

  • requester_id — заявитель, который оформил командировку в CRM;
  • user_id — командируемый сотрудник;
  • approver_id — согласующий, который должен подтвердить поездку.

Для совместимости user_id остается основным полем командируемого сотрудника. Если requester_id не передан при создании, API подставляет текущего пользователя из токена, а сервисный слой использует user_id как fallback.

Статусы ​

Базовая цепочка:

text
planned -> approved -> in_progress -> completed

Дополнительно из активных стадий можно перейти в cancelled.

Смысл статусов:

  • planned — заявка создана, поездка еще не подтверждена;
  • approved — согласующий подтвердил командировку;
  • in_progress — сотрудник находится в поездке;
  • completed — поездка закрыта, заполнены фактические даты;
  • cancelled — командировка отменена.

Переход planned -> in_progress закрыт: командировка должна быть согласована перед началом.

Матрица переходов (для UI) ​

Кнопки смены статуса в интерфейсе нужно показывать строго по этой матрице — PATCH с недопустимым переходом вернёт 403 (access_denied).

Из \ Вplannedapprovedin_progresscompletedcancelled
planned✓✓——✓
approved—✓✓—✓
in_progress——✓✓✓
completed———✓—
cancelled————✓

completed и cancelled — терминальные: из них выйти нельзя. Переход в completed требует заполненных departure_actual и return_actual (иначе 400).

Основные поля ​

  • проект: project_id — необязателен (командировка может быть не привязана к проекту; в этом случае попутные задачи и синхронизация с бюджетом не выполняются);
  • маршрут: from_location_id/to_location_id (выбор из справочника локаций) и/или from_address/to_address (произвольный текст). Можно выбрать локацию из списка, вписать адрес вручную, либо указать и то и другое; произвольный адрес имеет приоритет в подписях попутных задач;
  • цель поездки: purpose;
  • плановые даты: departure_planned, return_planned;
  • фактические даты: departure_actual, return_actual;
  • транспорт и проживание: transport_type, transport_details, accommodation;
  • плановые и фактические расходы: planned_cost, actual_cost;
  • служебные заметки: notes.

При переходе в approved API фиксирует approved_by_id текущим пользователем и approved_at текущим временем. После завершения командировки фактические расходы синхронизируются с финансовым контуром как источник trip.

Факт может превышать план: перерасход по источнику trip записывается в статью затрат как есть (раньше такое обновление отклонялось planned_amount_below_actual, и факт терялся). В интерфейсе фактические расходы вводятся только при редактировании, при создании — только плановый бюджет (билеты, жильё, суточные).

При отмене командировки (cancelled) ранее зарегистрированные плановые и фактические расходы по источнику trip обнуляются в бюджете проекта, чтобы отменённая поездка не раздувала смету.

Попутные задачи ​

Командировка — это не только запись, но и работа для других сотрудников. Система автоматически ставит попутные задачи в проект командировки:

Вид (kind)Когда создаётсяИсполнительСрок
transportпри переходе в approvedорганизатор (requester_id)дата выезда (departure_planned)
accommodationпри переходе в approvedорганизатордата выезда
advanceпри переходе в approvedорганизатордата выезда
expense_reportпри переходе в completedкомандируемый (user_id)возвращение + 3 дня

Подготовительные задачи (transport/accommodation/advance) создаются на организатора командировки; авансовый отчёт после возвращения — на самого командируемого. Если командировка создаётся сразу в статусе approved/in_progress/completed, соответствующие задачи ставятся при создании.

Срок подготовительных задач может быть раньше начала проекта — сотрудник выезжает к монтажу; проверка «не раньше начала проекта» для них не действует (tasks.CreateTaskInput.PreparationTask), срок в прошлом по-прежнему запрещён. Раньше такие задачи молча не создавались. То же у задач логиста по перевозке.

Названия и описания задач читаемые: «Купить билеты — командировка Павлов Иван Олегович, 15.09–18.09.2026», в описании — названия локаций и даты в формате ДД.ММ.ГГГГ по часовому поясу компании (APP_TIMEZONE; даты командировки хранятся как местная полночь, поэтому в UTC день сдвигался бы назад). Срок в прошлом (командировку завели задним числом) сдвигается на сегодня, иначе модуль задач отказал бы; сбой постановки задачи пишется в лог, а не проглатывается молча.

Генерация идемпотентна: на одну командировку приходится не более одной задачи каждого вида (уникальный индекс trip_tasks(trip_id, kind)), поэтому повторный PATCH тем же статусом не плодит дубликаты. Связи хранятся в таблице trip_tasks и возвращаются в карточке командировки в поле tasks (GET /api/v1/trips/:id).

Автогенерацию можно отключить переменной окружения APP_AUTO_CREATE_TRIP_TASKS=false (по умолчанию включена). Создание задач делается best-effort: сбой модуля задач не срывает смену статуса командировки.

API ​

  • POST /api/v1/trips — создать командировку;
  • GET /api/v1/trips — получить список с фильтрами project_id, user_id, requester_id, approver_id, status;
  • GET /api/v1/trips/:id — получить карточку;
  • PATCH /api/v1/trips/:id — обновить статус, согласующего, цель, маршрут (from_location_id/to_location_id, from_address/to_address), фактические даты и фактическую стоимость;
  • GET /api/v1/trips/export — выгрузить список в Excel.

Все эндпоинты требуют права trips.manage. Доступ к конкретной карточке есть администратору, командируемому, заявителю, согласующему и пользователям того же отдела (см. ниже).

Контракт ответа (карточка командировки) ​

Поля объекта Trip. Nullable-поля при отсутствии значения опускаются (omitempty), фронт должен трактовать их как необязательные.

ПолеТипNullableПримечание
idintegerнет
user_idintegerнеткомандируемый сотрудник
requester_idintegerдазаявитель
approver_idintegerдасогласующий
approved_by_idintegerдакто подтвердил (ставится автоматически)
approved_atstring (RFC3339)дамомент подтверждения
project_idintegerдапроект (необязателен)
department_idintegerдаотдел, оплачивающий поездку; не передан — основной отдел командируемого (не того, кто оформляет)
from_location_idintegerдаоткуда — локация из справочника
to_location_idintegerдакуда — локация из справочника
from_addressstringдаоткуда — произвольный адрес (до 500 символов)
to_addressstringдакуда — произвольный адрес (до 500 символов)
statusstring (enum)нетplanned/approved/in_progress/completed/cancelled
purposestringдацель поездки
transport_typestring (enum)даplane/train/car/bus/taxi/other
transport_detailsstringдарейс/поезд и т.п.
accommodationstringдагостиница/жильё
departure_plannedstring (RFC3339)даплановый выезд
return_plannedstring (RFC3339)даплановое возвращение
departure_actualstring (RFC3339)дафактический выезд
return_actualstring (RFC3339)дафактическое возвращение
planned_costnumberдаплановые расходы (деньги — число, 2 знака)
actual_costnumberдафактические расходы
notesstringдазаметки
tasksarrayдапопутные задачи; только в GET /api/v1/trips/:id
created_atstring (RFC3339)нет
updated_atstring (RFC3339)нет

Элемент массива tasks: { "task_id": integer, "kind": string }, где kind ∈ transport/accommodation/advance/expense_report. В списке (GET /api/v1/trips) и в ответах POST/PATCH поле tasks не возвращается — за списком попутных задач обращайтесь к карточке или к модулю задач по task_id.

Ошибки ​

КодКогда
400 invalid_requestнекорректные данные, нарушен порядок дат, нет фактических дат при completed
403 access_deniedнедопустимый переход статуса либо нет доступа к карточке
404 not_foundкомандировка не найдена
409 conflictу сотрудника уже есть пересекающаяся по датам командировка
400 trips_same_from_and_to«откуда» и «куда» — одно и то же место (при изменении проверяется итоговый маршрут)
400 trips_actual_date_in_futureфактическая дата выезда или возвращения позже завтрашнего дня: факт — то, что уже случилось

Рекомендации для UI и дизайна ​

  • Карточка vs список. Список (GET /api/v1/trips) не содержит tasks — блок «Попутные задачи» рисуем только на детальной карточке (GET /api/v1/trips/:id).
  • Кнопки статусов строим по матрице переходов; для терминальных completed/cancelled действий смены статуса не показываем.
  • Завершение поездки (-> completed) требует фактических дат: форму завершения делаем с обязательными departure_actual и return_actual, иначе бэкенд вернёт 400.
  • Попутные задачи. После одобрения карточка обзаводится тремя подготовительными задачами (билеты, жильё, аванс), после завершения — авансовым отчётом. В UI стоит показывать их статус-чипами со ссылкой на задачу по task_id. Иконки/цвета по kind: transport (билеты), accommodation (жильё), advance (деньги), expense_report (отчёт).
  • Проект необязателен. Поле выбора проекта в форме делаем опциональным — командировку можно создать без привязки к проекту (тогда не будет попутных задач и записи в бюджет).
  • Маршрут. Для «откуда»/«куда» даём выбор локации из справочника (*_location_id) и поле свободного ввода адреса (*_address). Допустимо заполнить любое из двух или оба; при наличии произвольного адреса он используется в подписях попутных задач.
  • Деньги (planned_cost/actual_cost) приходят числом с двумя знаками — форматируем как валюту, не как строку.
  • Пересечение дат. На 409 conflict показываем понятное сообщение, что у сотрудника уже есть командировка на эти даты, а не общую ошибку.

Пересечение дат. Командировка сотрудника не должна пересекаться с другой его командировкой (trips_overlapping_trip); отменённые не считаются. Форма проверяет это заранее и не даёт сохранить. Карточку командировки без права правки (не организатор, не финансы и не согласующий) форма открывает только для чтения.

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