Смета проекта
Смета — единственный источник цены проекта для клиента: версии со статусами, разделы, строки с себестоимостью и ценой, один расчёт на сервере (модуль internal/modules/estimates, миграция 0166). ТЗ и план: estimate-budget-documents-spec.md, этап 3.
В интерфейсе: карточка проекта → «ТЗ и смета» → Смета (рядом «ТЗ» и «Бюджет»).
Версии и статусы
| Статус | Что значит | Куда дальше |
|---|---|---|
draft черновик | правится: строки, разделы, наценка, скидки, налоги, «Собрать смету» | sent, approved |
sent отправлена клиенту | итоги сохранены снимком (totals) | approved, changes_requested, draft (вернуть в работу) |
approved согласована | действующая смета проекта | — (новая согласованная уводит её в archived) |
changes_requested нужны изменения | версия закрыта | новая версия из неё |
archived архив | прежняя согласованная | — |
- Черновик у проекта один (
409 estimate_draft_exists); новая версия — копия любой версии (from_id) или пустая смета со стандартными разделами. - Согласование: снимок итогов, прежняя согласованная → архив, плановый бюджет проекта = итог сметы для клиента (
projects.planned_budget). - Пустую смету (итог 0) не отправляют и не согласуют (
400 estimate_empty). Закрытый проект смету не меняет. - Действующая смета: согласованная, иначе последняя не в архиве (
GET /projects/:id/estimates/current).
Расчёт
Множитель периода = дни аренды со скидкой за длительность | дни | часы | 1
Себестоимость строки = кол-во × множитель × себестоимость за единицу
Цена строки = кол-во × множитель × цена для клиента за единицу
(цена не задана → себестоимость × (1 + наценка сметы), до рубля)
Раздел = Σ цен строк − скидка раздела (процент или сумма)
Сумма = Σ разделов − скидка сметы
УСН = Сумма × УСН %
НДС = (Сумма + УСН) × ставка — сверху (on_top) или «в том числе» (included)
Итого клиенту = Сумма + УСН + НДС сверху
Маржа = Сумма − Σ себестоимостейПример заказчика: 1 000 → УСН 10 % = 1 100 → НДС 5 % от 1 100 = 1 155 (calc_test.go).
- Скидка аренды за длительность — одна формула на всю систему (
money.RentalDayMultiplier): 1-й день ×1, дни 2–5 ×0,5, 6–10 ×0,3, 11–15 ×0,15, дальше ×0,1. Ей же считают заявки на оборудование. - Наценка одна на смету и клиенту не видна: она внутри цен строк без своей цены. Строка с заданной ценой наценку не получает.
- Внутренняя строка (
is_internal) — только себестоимость: субаренда и закупка своего оборудования, резерв на риски. Клиент её не видит и не платит. - Налоги новой сметы — от продавца: переопределение НДС проекта, иначе режим юрлица-продавца (плательщик проекта, иначе основной набор «Моей компании»); УСН — у реквизитов юрлица (
requisites.usn_percent), иначе из настроек бюджета; наценка — из настроек бюджета. В смете налоги и продавца можно поменять.
«Собрать смету»
POST /estimates/:id/collect — apply=false возвращает разницу (добавится, изменится, уберётся, исправлено вручную), apply=true применяет.
| Раздел | Источник | Себестоимость | Цена |
|---|---|---|---|
| Оборудование | потребность проекта (project_equipment_requirements) | 0 (своё) | цена проекта или каталога за день × дни аренды со скидкой × кол-во |
| Оборудование | субаренда и закупка — плановые статьи логистики (logistic_operation_supply) | плановая стоимость | внутренняя строка |
| Оборудование | разбор ТЗ помощником — позиции каталога, которых нет в потребности; не найденные — строкой без цены | 0 | цена каталога, 1 день |
| Перевозки | плановые статьи операций логистики | плановая стоимость | с наценкой |
| Командировки | плановые статьи командировок | плановая стоимость | с наценкой |
| Команда | последняя смета по ТЗ (часы по отделам) | часы × ставка отдела | часы × ставка отдела для клиента, иначе с наценкой |
| По категории | ручные плановые статьи бюджета | плановая сумма | с наценкой |
Строка из источника, исправленная вручную, фиксируется (is_manual) — сбор её не трогает; manual=false при правке возвращает её к данным проекта. Строка удалённого источника убирается при следующем сборе.
Загрузка из файла
«Загрузить из файла» — в пустой смете (создаёт черновик) и в меню «⋯» черновика. Строки из файла добавляются новыми разделами в конец; в смету ничего не пишется до «Добавить в смету».
POST /estimates/:id/import/parse(multipartfile, .xlsx или .csv, до 10 МБ) — разбор таблиц по заголовкам колонок: «Наименование», «Кол-во», «Ед.», «Цена», «Коэфф» / «Дней» / «Смен» (день) или «Часов» (час), «Стоимость» / «Сумма», «Себестоимость цена» / «Себестоимость сумма», «Примечание». Две таблицы рядом на листе (шаблон RMS: «Аренда оборудования» и «Работы») разделяются пустой колонкой; заголовок таблицы — строка над шапкой; строка только с названием внутри таблицы — подраздел; «Итого», скидки и налоги пропускаются. Коэффициент переносится периодом без скидки аренды за дни, поэтому сумма строки совпадает с файлом; коэффициент 1 — без периода. Цены нет — считается из суммы. Себестоимость берётся и с внутренних листов «(ВНУТР) …» по названию строки; одинаковые таблицы с разных листов берутся один раз. В файле есть «Скидка» — предупреждение: скидка задаётся у раздела или сметы руками.- Таблица не нашлась (
estimate_import_nothing) или файл — PDF, Word, скан — фронт отдаёт его помощнику:POST /ai/extract/estimate, ответ того же вида. - Предпросмотр: разделы и строки с галочками, итог по выбранному и по файлу; «Цены в файле — это»: «Цены для клиента» (цена строки; себестоимость — если видна) или «Себестоимость» (цену даёт наценка; только с доступом к затратам).
POST /estimates/:id/import— отмеченное одной транзакцией, строки ручные (is_manual): «Собрать смету» их не трогает.
Ставки сотрудников
user_rates— история ставок: себестоимость часа и ставка для клиента с даты (GET/POST/DELETE /users/:id/rates; правят финансы и администратор).- Действующая ставка в проекте: команда проекта (
hourly_rate/client_hourly_rate) → ставка сотрудника на сегодня → отдел (hourly_rate/client_hourly_rate). - Строка «Сотрудник» (
user_idвPOST /estimates/:id/lines) берёт эти ставки, если в строке не заданы свои.
Права
| Действие | Кто |
|---|---|
| Смотреть смету | доступ к бюджету клиента или затратам (client_budget / costs чтение) и к разделу финансов проекта |
| Себестоимость, маржа, внутренние строки | чтение затрат (costs), администратор, finance.manage |
| Править, собирать, менять статус | кто ведёт бюджет клиента этого проекта (client_budget запись + деньги проекта), финансы (client_budget «все»), администратор, finance.manage, documents.manage |
| Ставка для клиента отдела и в команде | видна с client_budget, себестоимость — с costs |
Ссылка клиенту
Смета → «Отправить клиенту» (POST /estimates/:id/link): ссылка {FRONTEND_BASE_URL}/estimate/<token>. Черновик при этом становится «отправлена клиенту» (снимок итогов). Клиент без входа в RMS:
- видит смету: наша компания и заказчик, разделы и строки с ценами, скидки, УСН и НДС, итог — без себестоимости, наценки, внутренних строк и комментариев;
- скачивает PDF («СМЕТА (версия N)», без номера — номер получает только выпущенный документ, место для подписей сторон);
- «Согласовать» (ФИО обязательно, комментарий) или «Нужны изменения» (комментарий обязательно). Решение одно на ссылку и только по отправленной версии: согласование — как менеджером (плановый бюджет проекта, архив прежней версии), «нужны изменения» закрывает версию — дальше новая.
Токен — 32 случайных байта; в estimate_links — sha256 для поиска и зашифрованная копия (APP_SECRETS_KEY), чтобы менеджер мог снова скопировать ссылку (без ключа — только сразу после создания). Активная ссылка у версии одна: новая отзывает прежнюю; «Отозвать» и новая версия сметы делают ссылку недействительной (404 estimate_link_not_found), как и возврат версии в черновик.
Менеджер (руководитель проекта и автор ссылки) получает уведомления: клиент открыл ссылку (первый раз), согласовал, просит изменения — и экран сметы обновляется сам (часть estimate). В окне ссылки — сколько раз и когда открыта, решение клиента. Ссылку можно отправить письмом (POST /estimates/:id/link/send), если на сервере включена почта (EMAIL_ENABLED), иначе 409 estimate_mail_disabled.
Публичные маршруты — GET /public/estimates/:token, GET …/pdf, POST …/decision — без авторизации, с лимитом по адресу: 60 просмотров и 10 решений в минуту (429). В журнале — создание ссылки и решение клиента (ФИО, комментарий, IP, браузер).
Документы и перенос
- Смета PDF и счёт на всю смету (аванс, остаток) берут строки и итоги из действующей сметы; без сметы — из бюджета, как раньше. См. Систему документов.
- Перенос при запуске: проекту без сметы с плановыми статьями или потребностью в оборудовании собирается смета v1 (если по проекту выставлены счета — сразу «согласована», плановый бюджет не трогается). Закрытые и архивные проекты не переносятся.
- Журнал: создание версии, смена статуса, удаление черновика (
entity_type = estimate). Живые обновления — частьestimateпроекта.