Skip to content

Система бюджета и сметы проекта ​

Сейчас

План проекта — смета (модуль estimates), факт — журнал расходов; сравнение по разделам — План и факт. Ниже — устройство журнала расходов (cost_items) и прежней сводки бюджета по категориям.

Это описание того, как CRM RMS показывает экономику проекта: от отдельных статей затрат до сметы с наценкой, финансовой сводки и документов.


Общая схема ​

В текущей версии backend бюджетный контур разделен на два слоя:

  • /api/v1/finance/... — источник финансовых строк проекта: cost_items, выручка и P&L-сводка.
  • /api/v1/projects/:id/budget — агрегированная смета проекта: строки по категориям, subtotal, наценка и итог.

budget не обходит задачи, логистику и командировки напрямую. Он читает finance.cost_items, группирует их по категориям и применяет настройки проекта из project_budget_settings.

Логистика: "Подтверждена доставка на 15 000"
    ↓
Finance: "Создаю/обновляю cost item source_type=logistic_operation"
    ↓
Командировка: "План/факт поездки 8 000"
    ↓
Finance: "Создаю/обновляю cost item source_type=trip"
    ↓
Менеджер: "Добавляю ручную статью подрядчика"
    ↓
Finance: "Создаю cost item без source_type"
    ↓
Budget: "Группирую cost items по категориям и считаю наценку"
    ↓
Documents: "Генерирую estimate/invoice/act из проекта, бюджета и реквизитов"

Статья затрат (CostItem) ​

CostItem — атомарная финансовая строка проекта.

ПолеСмысл
project_idПроект, к которому относится затрата
categoryКатегория из фиксированного списка
planned_amountПлановая сумма
actual_amountФактическая сумма
source_type / source_idДоменный источник, если строка создана автоматически
notesПояснение для ручной или уточненной строки

Категории, поддержанные backend:

CategoryЧто обычно означает
techТехника, оборудование, материалы
logisticsДоставка и логистические операции
travelКомандировки
laborТрудозатраты
rentАренда
salaryЗарплата и ФОТ
contractorsПодрядчики
materialsМатериалы
creativeКреатив и производство контента
officeОфисные расходы
otherПрочее
taxesНалоги

Сервис Finance проверяет суммы:

  • суммы не могут быть отрицательными;
  • в ручной статье план не может быть меньше факта; автоматические источники (командировки, логистика, часы) записывают факт как есть — перерасход виден как отклонение;
  • для проекта в статусе closed новые/обновленные cost items запрещены.

Источники затрат ​

Ручные статьи ​

Менеджер или финансовый пользователь создает строку напрямую:

  • POST /api/v1/finance/cost-items;
  • GET /api/v1/finance/cost-items?project_id=...;
  • GET /api/v1/finance/projects/:id/cost-items;
  • GET /api/v1/finance/projects/:id/cost-items/export.

Ручной cost item подходит для подрядчиков, аренды, материалов, налогов и любых затрат, которые пока не приходят из доменного workflow.

Правка — PATCH /api/v1/finance/cost-items/:id: отсутствующее поле не меняется, явный null очищает его (planned_amount, actual_amount, notes → NULL); пустое название тоже стирает заметку. Статья без плана — просто фактический расход (как при создании без planned_amount), правило «план ≥ факт» проверяется по итоговым значениям. Суммы принимаются с копейками.

Логистика ​

Логистическая операция пишет в Finance как:

text
category=logistics
source_type=logistic_operation
source_id=<operation_id>

Запись создается после финансового approve суммы логистики или при закрытии операции, когда известны плановая и фактическая стоимость. Детальный маршрут описан в Логистическом контуре.

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

Командировка пишет в Finance как:

text
category=travel
source_type=trip
source_id=<trip_id>

При создании командировки с проектом сервис сохраняет план/факт, если они есть. При переходе в completed факт обновляется. При cancelled сумма источника обнуляется, чтобы отмененная поездка не раздувала бюджет.

Учет времени (трудозатраты) ​

Каждая запись времени превращается в статью «Трудозатраты» (finance.SyncLaborCosts, internal/modules/finance/labor.go):

text
category=labor
source_type=time_entry
source_id=<time_entry_id>
actual_amount = hours × rate (округление до копеек)
planned_amount = NULL
notes = "Трудозатраты: 4.00 ч × 2500.00"

Ставка выбирается в порядке приоритета:

#ИсточникКогда
1project_team_members.hourly_rateПроектная ставка участника команды (> 0)
2compensation_profiles с kind=hourlyПочасовой профиль оплаты, действующий на дату записи
3compensation_profiles с kind=salaryОклад / норма часов в месяце (APP_LABOR_MONTHLY_NORM_HOURS, по умолчанию 168)

Без ставки статья не создаётся: такие часы остаются в учёте времени, но не в деньгах.

Когда пересчитывается: после POST /time-entries/self, POST /time-entries, после паузы и завершения рабочей сессии задачи и после утверждения табеля (для всех проектов, где сотрудник отмечал часы в этом месяце). Пересчёт идемпотентен и обновляет только изменившиеся строки. Сбой пересчёта не отменяет запись времени — строки догоняются при следующем изменении.

Плановая сумма у таких строк пустая: плановые трудозатраты должны приходить из оценки ТЗ (этап 7 плана), а не из факта. Закрытый проект (closed) не пересчитывается. Строки с источником — только для чтения: править их через PUT /finance/cost-items/:id нельзя.


Budget summary ​

GET /api/v1/projects/:id/budget возвращает агрегированную смету проекта.

Сервис делает три шага:

  1. Читает настройки бюджета проекта (markup_percent, notes).
  2. Получает все finance.cost_items проекта.
  3. Группирует суммы по категориям, считает subtotal, наценку, УСН, НДС и grand_total.

Формат результата:

json
{
  "project_id": 7,
  "lines": [
    {
      "category": "logistics",
      "description": "Логистика",
      "amount": 15000.0,
      "actual_amount": 14200.0
    }
  ],
  "subtotal": 15000.0,
  "markup_percent": 20,
  "markup_amount": 3000.0,
  "total": 18000.0,
  "display_lines": [
    {
      "category": "logistics",
      "description": "Логистика",
      "amount": 15000.0,
      "kind": "item"
    },
    {
      "category": "markup",
      "description": "Наценка (20.00%)",
      "amount": 3000.0,
      "kind": "markup"
    },
    {
      "category": "taxes",
      "description": "УСН (10.00%)",
      "amount": 1800.0,
      "kind": "tax",
      "tax_type": "usn"
    },
    {
      "category": "taxes",
      "description": "НДС (5.00%)",
      "amount": 990.0,
      "kind": "tax",
      "tax_type": "vat"
    }
  ],
  "usn_percent": 10,
  "usn_amount": 1800.0,
  "vat_mode": "on_top",
  "vat_rate": 5,
  "vat_amount": 990.0,
  "taxes": {
    "usn": {
      "type": "usn",
      "display_mode": "separate",
      "base_amount": 18000.0,
      "amount": 1800.0,
      "affects_total": true,
      "included_in_lines": false
    },
    "vat": {
      "type": "vat",
      "display_mode": "separate",
      "base_amount": 19800.0,
      "amount": 990.0,
      "affects_total": true,
      "included_in_lines": false
    }
  },
  "grand_total": 20790.0
}

Важно: в текущей реализации subtotal и total строятся по плановым суммам (planned_amount). Фактические суммы отдаются в строках как actual_amount, чтобы UI мог показать план/факт, но они не заменяют плановый subtotal.

lines остаётся сырой группировкой статей затрат. display_lines — представление для UI и проектных документов: наценка добавляется строкой kind: "markup"; если налог скрыт в позициях, суммы строк увеличены пропорционально; если налог показывается отдельно и увеличивает итог, в конец добавляется строка kind: "tax".

Порядок налогового расчёта фиксированный: сумма позиций после markup_percent → УСН → НДС. Например, позиция 1000, УСН 10%, НДС on_top 5% даёт 1000 + 100 + 55 = 1155.


Наценка ​

Настройки проекта читаются и обновляются через:

  • GET /api/v1/projects/:id/budget/settings;
  • PUT /api/v1/projects/:id/budget/settings.

Если настройки ещё не сохранялись, GET возвращает 200 с нулевой наценкой и режимами отображения налогов separate. Отсутствие строки настроек не считается ошибкой и не требует предварительного PUT.

Payload:

json
{
  "markup_percent": 20,
  "usn_percent": 10,
  "usn_display_mode": "separate",
  "vat_display_mode": "included",
  "notes": "Коммерческая наценка для клиента"
}

markup_percent допускает диапазон 0..10000 и хранится с точностью 10 знаков после запятой (migrations/0156): итог сметы, заданный в интерфейсе суммой, пересчитывается в процент без сдвига на рубли. usn_percent — 0..100. Настройки закрытого проекта не меняются: PUT отвечает 409 budget_project_closed. Менять настройки могут пишущие client_budget или costs. Режимы usn_display_mode и vat_display_mode принимают значения:

  • separate — налог показывается отдельной строкой, если он увеличивает итог;
  • included — налог распределяется по позициям, если это additive-налог (УСН, НДС on_top).

В интерфейсе проекта галочка «Добавить к цене позиций» включает УСН и сохраняет usn_display_mode=included; ставка по умолчанию — 6%, но её можно изменить. Это не меняет сырые cost_items: налог пропорционально, с копеечным округлением, добавляется к display_lines и к строкам детального PDF.

Для vat_mode = included НДС всегда только выделяется из текущей суммы и не увеличивает grand_total; режим отображения управляет тем, показывать ли его отдельно как "в том числе" или считать уже включённым в строки.


Финансовая сводка проекта ​

GET /api/v1/finance/projects/:id/summary возвращает P&L:

  • planned_revenue;
  • actual_revenue;
  • planned_costs;
  • actual_costs;
  • planned_profit;
  • actual_profit.

Это отдельная финансовая сводка, не то же самое, что budget summary. Budget нужен для сметы с наценкой, Finance summary — для управленческого контроля прибыли.


Документы и смета ​

PDF/документ сметы создается через:

  • POST /api/v1/projects/:id/documents/estimate.

Генератор берет:

  • проект и клиента;
  • budget summary;
  • cost items как источник строк;
  • primary internal-реквизиты поставщика;
  • primary-реквизиты компании-клиента.

Если реквизиты не заполнены, документ может сгенерироваться с пустыми юридическими строками. Если не удалось прочитать бюджет, cost items или реквизиты, генерация должна остановиться, чтобы не выпустить некорректный документ.


Доступ ​

Финансовые и бюджетные endpoint-ы проверяют доступ к секции finance проекта.

Доступ есть:

  • admin;
  • пользователям с глобальным finance.manage в рамках доступного проекта;
  • менеджеру проекта;
  • участникам команды с проектными финансовыми ролями: manager, lead, accountant, finance и поддержанными legacy-алиасами.

Обычный участник команды не получает финансовую секцию только из-за участия в проекте.


Рекомендации для фронта ​

Экран бюджета ​

  1. Загружайте GET /api/v1/projects/:id/budget для агрегированной сметы.
  2. Загружайте GET /api/v1/finance/projects/:id/cost-items для детализации и редактирования строк.
  3. Разделяйте "плановую смету" и "факт": subtotal строится по плану, фактические суммы показываются отдельно.
  4. Если в проекте есть time entries, но нет labor cost items, показывайте часы отдельно от денег или явно сообщайте, что трудовые затраты еще не заведены в Finance.
  5. Для Excel используйте GET /api/v1/finance/projects/:id/cost-items/export. Колонки совпадают с таблицей «Статьи затрат»: «Статья», «Категория», «Источник», «План, ₽», «Факт, ₽», «Комментарий» — с русскими подписями категорий и источников, а не кодами.

Форма cost item ​

Проверяйте на клиенте те же правила, что backend:

  • category только из списка;
  • суммы неотрицательные;
  • planned_amount >= actual_amount, если обе суммы указаны; пустой план уходит как null (создание — поле не передаётся), а не как 0;
  • для закрытого проекта форму создания/редактирования лучше скрыть.

Ключевые правила финансового контура ​

1. Budget агрегирует Finance ​

Если строка не попала в finance.cost_items, она не появится в projects/:id/budget.

2. Source защищает от дублей ​

Для автоматических источников используется пара source_type + source_id. Повторная синхронизация логистики или командировки обновляет существующую строку, а не создает дубль.

3. Closed проект финансово заморожен ​

Статус проекта closed запрещает новые cost items и изменения источников через Finance.

4. Деньги считаются десятично ​

Все суммы в API приходят как JSON number, но внутри считаются через money.Money и NUMERIC, без float64 для бизнес-арифметики.


Резюме: как появляется смета ​

text
Cost Items
  manual / logistic_operation / logistic_operation_supply / trip / time_entry
    ↓
Finance
  planned_amount + actual_amount + category
    ↓
Budget Summary
  lines + subtotal + markup + total
    ↓
Documents
  estimate / invoice / act
    ↓
Project Control
  план, факт, прибыль, закрытие проекта

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