Гибкий НДС и «Моя компания»
Налог на проекте задаётся гибко: ставка и режим НДС хранятся на реквизитах нашей компании («Моя компания»), наследуются проектом при выборе компании-плательщика и могут быть переопределены прямо в проекте. От выбранного режима зависит, как считается смета и формируются ли документы с налогом.
Налоговый режим (vat_mode)
| Режим | Смысл | Как считается НДС |
|---|---|---|
none | Без НДС | Налог = 0, документы без налоговой строки |
included | НДС включён в цену и выделяется из неё (по умолчанию) | НДС = сумма × ставка / (100 + ставка) |
on_top | НДС начисляется сверху на цену | НДС = сумма × ставка / 100 |
vat_rate — ставка в процентах (по умолчанию действующая ставка 22).
Расчёт выполняется точной десятичной арифметикой в internal/money (money.ComputeVAT), без float64. См. Деньги, округление и НДС.
Где это хранится
«Моя компания» — реквизиты-исполнитель
Наборы реквизитов нашей компании (owner_type = 'internal') администрируются через Настройки → Моя компания:
GET /api/v1/settings/my-company/requisites
POST /api/v1/settings/my-company/requisites
GET /api/v1/settings/my-company/requisites/{id}
PUT /api/v1/settings/my-company/requisites/{id}
DELETE /api/v1/settings/my-company/requisites/{id}
PATCH /api/v1/settings/my-company/requisites/{id}/set-primaryКаждый набор несёт юридические/банковские данные и налоговые поля vat_mode + vat_rate. Так одна и та же компания может вести и проекты с НДС, и без — заведите два набора с разными режимами. Читают реквизиты все сотрудники, а управляющие эндпоинты (POST/PUT/DELETE/set-primary) доступны только финансам: запись затрат «все» (OWNER, OPS, FIN_DIR) или глобальный администратор (transport.IsFinance). Юристы и остальные получают 403 requisites_my_company_finance_only. То же правило действует для записи owner_type=internal через общий /api/v1/requisites; реквизиты клиентов и поставщиков по-прежнему правит тот, у кого запись справочника контрагентов. Интерфейс прячет кнопки по capability my_company_requisites_manage.
Чтение списка для формы проекта (доступно всем)
Чтобы заполнить выбор «Моя компания» в форме проекта, есть read-only маршруты. Читают их все аутентифицированные сотрудники — как и /settings/my-company/requisites (раньше требовалось projects.manage):
GET /my-company/requisites — список наборов (для дропдауна)
GET /my-company/requisites/:id — конкретный наборФормат ответа идентичен админским (массив объектов Requisite). Используйте id + name, бейджем можно показать vat_mode/vat_rate.
Все маршруты /settings/my-company/* и /my-company/requisites присутствуют в OpenAPI/Scalar и в Postman/Insomnia-коллекциях (генератор контрактов теперь сканирует все функции Register*Routes).
Проект — выбор компании и переопределение
При создании/редактировании проекта:
| Поле | Тип | Назначение |
|---|---|---|
payer_requisite_id | int64? | Выбранная «Моя компания» (набор internal-реквизитов). |
vat_mode | string? | Переопределение режима. null → наследуется от payer_requisite_id. |
vat_rate | number? | Переопределение ставки. null → наследуется. |
payer_requisite_id валидируется: он должен ссылаться на существующий internal-набор, иначе 400.
Сброс к наследованию
Переопределение НДС можно снять, вернувшись к режиму выбранной компании:
vat_mode: "inherit"— сбрасывает и режим, и ставку проекта вNULL(НДС снова наследуется отpayer_requisite_id).payer_requisite_id: 0— снимает выбранную «Мою компанию» (вNULL).
При создании проекта эти сентинелы эквивалентны «не задано».
Действующий (эффективный) НДС
Эффективный режим проекта = переопределение проекта, иначе режим компании-продавца — выбранной «Моей компании», а если она не выбрана — основного набора (от его имени выпускаются документы, см. «Система документов»), иначе none:
seller = requisites WHERE owner_type='internal'
ORDER BY (id = project.payer_requisite_id) DESC, is_primary DESC, id LIMIT 1
vat_mode = COALESCE(project.vat_mode, seller.vat_mode, 'none')
vat_rate = COALESCE(project.vat_rate, seller.vat_rate, 0)До 2026-09-30 без выбранного плательщика НДС не считался вовсе, даже если основная компания работает с НДС.
Эти значения возвращаются в сводке бюджета и используются документами.
Сводка бюджета
GET /api/v1/projects/{id}/budget отдаёт НДС, рассчитанный после наценки и УСН: сначала считается total (позиции + markup_percent), затем УСН, затем НДС.
{
"subtotal": 4583.33,
"markup_percent": 0,
"markup_amount": 0,
"total": 5500.00,
"vat_mode": "included",
"vat_rate": 20,
"vat_amount": 916.67,
"total_net": 4583.33,
"total_with_vat": 5500.00,
"grand_total": 5500.00
}included— налог выделен из суммы после УСН и не увеличиваетgrand_total.on_top—grand_total = total + usn_amount + vat_amount.none—vat_amount = 0,grand_total = total + usn_amount.
Отображение НДС в строках сметы управляется отдельно настройкой бюджета vat_display_mode (separate или included). Это не меняет юридический режим НДС: vat_mode отвечает за арифметику налога, vat_display_mode — только за представление в UI и проектных документах.
Где смотреть в коде
- Налоговые поля реквизитов:
migrations/0116_requisites_tax.sql,internal/modules/requisites/. - Выбор плательщика и переопределение на проекте:
migrations/0117_projects_payer_tax.sql,internal/modules/projects/(EffectiveVAT,validatePayerAndVAT). - Расчёт НДС в сводке:
internal/modules/budget/service.go. - Хелперы НДС:
internal/money/money.go(ComputeVAT,VATModes).