Черновик спецификации: адресуемые данные проекта для документов
Актуально на: 2026-07-09
Статус: draft для обсуждения.
1. Задача
Нужно уметь получить из проекта любое пользовательское или расчетное значение и вставить его в Word, PowerPoint или другой документ по специальному символическому обозначению.
Документ должен обновляться без ручной правки цифр: если проект изменился, генератор заново разрешает все символы и подставляет актуальные значения.
2. Текущее состояние в проекте
2.1. Что уже хранится в БД
В Prisma уже есть нормализованные таблицы для большой части вводимых данных:
| Область | Таблицы |
|---|---|
| Проект | Project, ProjectInfo, ProjectSettings |
| Продукты и продажи | ProjectProduct, ProjectProductSalesVolume |
| Налоги | ProjectTaxSettings, ProjectTaxItem, ProjectTaxRatePeriod, ProjectTaxManualAmount, ProjectTaxRegimeLimit |
| Персонал | ProjectPersonnelSettings, ProjectPersonnelEmployee, ProjectPersonnelEmployeePayoutItem, ProjectPersonnelBonus |
| Общие расходы | ProjectGeneralExpenseSettings, ProjectGeneralExpense, ProjectGeneralExpenseScheduleItem |
| Акционерный капитал | ProjectEquityParticipant, ProjectEquityContributionItem, ProjectEquityPayoutItem |
| Legacy/backfill | ProjectWorkspaceState.state |
Важно: ProjectWorkspaceState.state остается fallback/legacy-источником. Новая система подстановок не должна напрямую зависеть от UI-state, но должна уметь читать legacy-поля, пока не все области нормализованы.
2.2. Что уже рассчитывается в коде
Расчетные величины сейчас собираются не в БД, а в моделях:
| Область | Источник расчета |
|---|---|
| Cash-flow | lib/workspace/use-results-cashflow-model.ts, lib/workspace/results-cashflow-model.ts |
| P&L | lib/workspace/equity-model.ts через buildProfitLossRows |
| Налоги | lib/workspace/tax-model.ts |
| Займы | lib/workspace/results-cashflow-model.ts |
| Капитал участников | lib/workspace/equity-model.ts |
| Эффективность инвестиций | lib/workspace/investment-efficiency-model.ts |
Вывод: для документов нужен единый расчетный слой, который повторяет серверно тот же pipeline, что и экран workspace.
3. Базовый принцип
Любое значение должно иметь стабильный ключ:
domain.metric[selector].property@period | formatДля документов предлагается использовать двойные фигурные скобки:
{{ pf:project.title }}
{{ pf:cashflow.balance_end@2026-08 | money }}
{{ pf:investment.npv[mode=real,scope=before_financing] | money }}Префикс pf: нужен, чтобы отличать наши токены от обычного текста и от возможных полей Word.
4. Периоды
Значения можно запрашивать тремя способами:
| Вид | Пример | Правило |
|---|---|---|
| По индексу периода | @p7 | 1-based номер периода проекта |
| По месяцу проекта | @2026-08 | календарный месяц YYYY-MM |
| По диапазону | @2026-02..2026-08 | агрегат за диапазон |
| По всем периодам | @all | агрегат за весь проект |
| На последний период | @last | последнее значение ряда |
Если период не указан:
- для scalar-полей возвращается само поле;
- для рядов ошибка
PERIOD_REQUIRED, кроме метрик, где есть естественный агрегат за весь проект.
5. Форматы
Формат указывается после |.
| Формат | Пример результата |
|---|---|
raw | 3472508.12 |
number | 3 472 508 |
money | 3 472 508 RUB |
percent | 21,5% |
ratio | 3,21 |
period | 2026-08 |
date | август 2026 |
text | без числового форматирования |
Дополнительные параметры формата:
{{ pf:investment.npv[mode=real] | money:currency=RUB,decimals=0 }}
{{ pf:investment.irr | percent:decimals=1 }}6. Селекторы
Селекторы идут в квадратных скобках.
{{ pf:sales.revenue[product=product-id]@2026-08 | money }}
{{ pf:cashflow.row[row=28]@p6 | money }}
{{ pf:investment.npv[scope=before_financing,mode=real] | money }}Общие селекторы:
| Селектор | Значения |
|---|---|
mode | nominal, real |
scope | before_financing, after_financing, participant |
periodicity | month, quarter, year |
row | id строки отчета, например 28 |
product | id продукта |
mik | id МиК |
employee | id сотрудника |
expense | id общей статьи расходов |
loan | id займа |
equity | id участника капитала |
7. Первый каталог ключей
Это не полный каталог, а стартовый набор для обсуждения.
7.1. Проект
| Токен | Что возвращает |
|---|---|
{{ pf:project.id }} | id проекта |
{{ pf:project.title }} | название проекта |
{{ pf:project.company }} | компания |
{{ pf:project.industry }} | отрасль |
{{ pf:project.region }} | регион |
{{ pf:project.start_date | date }} | дата старта |
{{ pf:project.duration_months }} | длительность в месяцах |
{{ pf:project.currency.code }} | код валюты |
7.2. Настройки расчета
| Токен | Что возвращает |
|---|---|
{{ pf:calc.discount.monthly | percent }} | месячная ставка дисконтирования compound |
{{ pf:calc.discount.yearly | percent }} | годовая ставка дисконтирования compound |
{{ pf:calc.simple.yearly | percent }} | простая годовая ставка |
7.3. Продажи
| Токен | Что возвращает |
|---|---|
{{ pf:sales.volume[product=ID]@2026-08 | number }} | объем продаж продукта за месяц |
{{ pf:sales.price[product=ID]@2026-08 | money }} | цена продукта в периоде |
{{ pf:sales.revenue[product=ID]@2026-08 | money }} | выручка продукта |
{{ pf:sales.revenue@2026-08 | money }} | общая выручка |
{{ pf:sales.revenue@all | money }} | выручка за весь проект |
7.4. Cash-flow
| Токен | Что возвращает |
|---|---|
{{ pf:cashflow.operating@2026-08 | money }} | операционный CF периода |
{{ pf:cashflow.investing@2026-08 | money }} | инвестиционный CF периода |
{{ pf:cashflow.financing@2026-08 | money }} | финансовый CF периода |
{{ pf:cashflow.period@2026-08 | money }} | денежный поток периода |
{{ pf:cashflow.balance_start@2026-08 | money }} | баланс на начало периода |
{{ pf:cashflow.balance_end@2026-08 | money }} | баланс на конец периода |
{{ pf:cashflow.row[row=28]@2026-08 | money }} | значение строки cash-flow по id |
7.5. P&L
| Токен | Что возвращает |
|---|---|
{{ pf:pl.revenue@2026-08 | money }} | выручка |
{{ pf:pl.gross_profit@2026-08 | money }} | валовая прибыль |
{{ pf:pl.ebitda@2026-08 | money }} | EBITDA |
{{ pf:pl.net_profit@2026-08 | money }} | чистая прибыль |
7.6. Эффективность инвестиций
| Токен | Что возвращает |
|---|---|
{{ pf:investment.npv[mode=nominal,scope=before_financing] | money }} | NPV без дисконтирования |
{{ pf:investment.npv[mode=real,scope=before_financing] | money }} | NPV с учетом ставки проекта |
{{ pf:investment.pi[mode=nominal,scope=before_financing] | ratio }} | PI без дисконтирования |
{{ pf:investment.pi[mode=real,scope=before_financing] | ratio }} | PI с учетом ставки проекта |
{{ pf:investment.pp[scope=before_financing] | period }} | простой срок окупаемости |
{{ pf:investment.dpp[mode=real,scope=before_financing] | period }} | дисконтированный срок окупаемости |
{{ pf:investment.irr[scope=before_financing] | percent }} | IRR. Не зависит от mode |
{{ pf:investment.roi[participant=ID,mode=nominal] | percent }} | ROI участника |
7.7. Финансирование
| Токен | Что возвращает |
|---|---|
{{ pf:loan.draw[loan=ID]@2026-08 | money }} | выдача займа |
{{ pf:loan.payment[loan=ID]@2026-08 | money }} | платеж по займу |
{{ pf:loan.interest[loan=ID]@2026-08 | money }} | проценты |
{{ pf:loan.debt_end[loan=ID]@2026-08 | money }} | долг на конец периода |
{{ pf:equity.contribution[equity=ID]@2026-08 | money }} | взнос участника |
{{ pf:equity.payout[equity=ID]@2026-08 | money }} | выплата участнику |
8. Требования к реализации
Нужны три слоя.
8.1. ProjectDataResolver
Серверный модуль:
resolveProjectValue({
projectId,
token: "pf:investment.npv[mode=real,scope=before_financing]",
period: "2026-08",
format: "money",
})Должен возвращать:
{
ok: true,
value: 3472508.12,
formatted: "3 472 508 RUB",
meta: {
source: "calculated",
period: "2026-08",
dependencies: ["cashflow", "projectCalc"]
}
}Ошибки:
| Код | Когда |
|---|---|
UNKNOWN_TOKEN | ключ не зарегистрирован |
PERIOD_REQUIRED | нужен период, но он не указан |
PERIOD_NOT_FOUND | период не найден в проекте |
ENTITY_NOT_FOUND | не найден product/loan/equity/etc |
VALUE_NOT_AVAILABLE | значение невозможно рассчитать |
AMBIGUOUS_SELECTOR | не хватает селектора |
8.2. ProjectDataRegistry
Каталог всех ключей:
{
key: "investment.npv",
title: "NPV проекта",
type: "money",
source: "calculated",
periodMode: "aggregate",
selectors: ["mode", "scope"],
resolver: resolveInvestmentNpv,
}Этот каталог нужен для:
- автодополнения токенов;
- проверки шаблонов перед генерацией;
- документации;
- миграции ключей без поломки старых документов.
8.3. ProjectSnapshot
Один объект, который собирает:
- вводимые данные из БД;
- legacy fallback из
ProjectWorkspaceState; - расчетные модели cash-flow, P&L, налоги, займы, капитал, эффективность;
- карту периодов проекта.
Документ не должен сам собирать расчеты. Он должен только спрашивать resolver.
9. Проверка доступности данных
Для каждого ключа в registry нужны тесты:
| Проверка | Описание |
|---|---|
canResolve | токен успешно возвращает значение на тестовом проекте |
periodByIndex | @pN и @YYYY-MM дают один период |
formatting | формат money/percent/ratio стабилен |
sourceTrace | значение возвращает источник и зависимости |
templateValidation | неизвестные токены ловятся до генерации документа |
10. Важные решения для обсуждения
- Синтаксис токенов: оставить
{{ pf:... }}или выбрать другой. - Периоды: использовать
@p7,@2026-08,@last,@all. - Нужно ли разрешать русские алиасы, например
{{ ПФ:NPV }}, или держать только стабильные английские ключи. - Нужно ли сохранять рассчитанные snapshots в БД для аудита, или всегда пересчитывать live.
- Как версионировать ключи, если формула поменялась.
11. Предлагаемый следующий шаг
- Создать
lib/project-data/registry.tsс первым набором ключей. - Создать
lib/project-data/resolver.ts. - Вынести server-safe сборку расчетного
ProjectSnapshotиз текущих UI-хуков. - Добавить API:
POST /api/projects/:projectId/resolve-valuesТело:
{
"tokens": [
"{{ pf:project.title }}",
"{{ pf:investment.npv[mode=real,scope=before_financing] | money }}",
"{{ pf:cashflow.balance_end@2026-08 | money }}"
]
}Ответ:
{
"{{ pf:project.title }}": "Магазин ...",
"{{ pf:investment.npv[mode=real,scope=before_financing] | money }}": "3 472 508 RUB",
"{{ pf:cashflow.balance_end@2026-08 | money }}": "..."
}