AURORA-1900: единые подписи «тыс. руб.» и НДС на экране и в выгрузке #291

Merged
aep merged 2 commits from feat/289-aurora-1900-unit-vat-labels into test 2026-11-02 20:48:37 +03:00
Owner

Closes #289.

Стек веток (важно для ревью)

Эта ветка стоит не на test, а поверх PR #288 (AURORA-1899, модуль
web/src/components/RealtimeTable/utils/moneyUtils.js с getMoneyUnit/
getDisplayFactor/UNIT_IN_RUB/AHR_LIMIT_SHEET, переписанный
useDenomination.js с дефолтом 'thousand' и ключом _denomination_v2,
displayFactor в tableColumns.jsx/Cell.jsx).

test → PR #288 (AURORA-1899) → эта ветка (коммит 24ca441).

Диф этого PR временно включает коммит #288 (он ещё не в test). PR создан
с --base test по процессу; после мерджа #288 ревьюеру будет проще смотреть
только коммит 24ca441.

Проблема

Единица и учёт НДС в заголовках денежных колонок подписаны по-разному на
разных формах, где-то не подписаны вовсе. Выгрузка в Excel расходится с
экраном: export_mappings.py всегда писал «тыс. руб.» (денежные значения в
базе хранятся в тыс. руб., денежный селектор на экран не влияет на выгрузку),
при этом в 96 местах — с опечаткой «с учетом НДC» (латинская C).

Что сделано

Экран

  • formatHeaderUnit(header, denomination) (headerYearUtils.js, рядом с
    formatHeaderYear) — подставляет в заголовок плейсхолдер {unit} по
    разрядности: «руб.», «тыс. руб.», «млн руб.», «млрд руб.». Переиспользует
    resolveDenomination/UNIT_IN_RUB из AURORA-1899, не дублирует маппинг
    единиц. Без переданной разрядности — литерал «тыс. руб.» (дефолт).
  • TableHead.jsx: formatHeaderUnit вызывается сразу после
    formatHeaderYear, разрядность приходит новым пропом denomination
    (проброшен из RealtimeTable.jsx).
  • CollapsibleTree.jsx, templateColumns.js (шаблоны этапов): литерал
    «тыс. руб.» — там нет живого контекста разрядности.
  • {unit} вместо «тыс. руб.» в заголовках FORM_1 (OPER/AHR/CAP/
    AHR_P_NO_LOCAL/CAP_NO_LOCAL), FORM_4 (AHR/CAP/OPER/STRUCTURE) — скриптовый
    проход + ручная проверка диффа. AHR_LIMIT.js не трогали — рублёвое
    исключение AURORA-1899/#288.
  • Добавили единицу, где её не было: FORM_1/FORM_4 SMETA (топ-группы «Расходы,
    планируемые...», «Итого», «Утверждённые...», «Фактические...»,
    «Скорректированная...» → , {unit}); FORM_2 OPER/AHR/CAP («Планирование
    годовых показателей» → , {unit} (без НДС)); FORM_2 SMETA («В тыс.
    рублей» → «В {unit} (без НДС)»); FORM_2 АХР аренда/коммуналка/охрана (план
    «Планируемая сумма расходов на {year} год» → , {unit}; факт «В тыс.
    рублей» → {unit}); FORM_2 OTCH9F («Сумма (тыс. руб.)» → «Сумма
    ({unit})», «Тыс. руб.» → {unit}); PROJECT CURRENT_EXPENSES/LIMIT (три
    группы «...по ${planTitle}» → , {unit}).
  • Свод 1 (SvodPage.jsx + SummaryTable.jsx): метки METRICS/
    FORM_3_METRIC_LABELS получили , {unit}; scaleColumns в
    SummaryTable.jsx теперь прогоняет заголовки через formatHeaderUnit с
    живой разрядностью (раньше трогал только значения ячеек).
  • Свод 2 (Svod2Page.jsx): «тыс. руб.» → {unit} в трёх заголовках
    (payment_amount, contract.ceiling, contract.expenses_qN);
    scaleNumericColumns аналогично прогоняет заголовки через
    formatHeaderUnit.
  • НДС-формулировки на экране не менял — вопрос 1 из тикета оставлен как
    есть (default: экран не трогаем, выгрузку подгоняем под экран).

Выгрузка (export_mappings.py / export_service.py)

  • FORM1_DEPTH_COLUMNS: «с учетом НДC» (латинская C, 96 мест) →
    плейсхолдер {vat}; «План {year}(тыс. руб.) с учетом НДС» → «План {year}
    (тыс. руб.) {vat_plan}» (плейсхолдер только для НДС-части — единица в
    выгрузке всегда литерал «тыс. руб.», разрядность не экспортируется).
    Правка этого словаря автоматически применяется к FORM4_DEPTH_COLUMNS/
    FORM4_STRUCTURE_DEPTH_COLUMNS/FORM4_OPER_DEPTH_COLUMNS — они строятся
    из него через _rename_depth_columns/copy.deepcopy при загрузке модуля.
  • render_column_years(columns, year, extra=None): необязательный extra
    — дополнительные плейсхолдеры (например {vat}/{vat_plan}), резолвятся
    той же функцией _replace_column_year_placeholders, что и {year}.
  • _create_xlsxwriter_sheet_writers: по листу передаёт нужный extra —
    Ф.1 OPER/CAP → «с учётом НДС»; Ф.1 AHR и все листы Ф.4 (OPER/AHR/CAP/
    STRUCTURE) → «без НДС» (соответствует экрану).
  • SMETA_COLUMNS: ко всем денежным меткам (кроме section_code/name)
    добавлено «, тыс. руб.».
  • Своды (export_summary_payload): в строку метаданных добавлена
    ("Единица", "тыс. руб.").

Тесты

  • node --test src/components/RealtimeTable/utils/*.test.js — 55/55
    зелёных
    (51 существовавших + 4 новых на formatHeaderUnit:
    подстановка по разрядности, заголовок без плейсхолдера не меняется,
    дефолт на undefined/null/'').
  • npm run build — без ошибок.
  • pytest tests/unit (venv с зависимостями из requirements-dev.txt) —
    102 passed, 15 из них новые в tests/unit/test_export_mappings_labels.py
    (нет латинской C в НДС ни при каком vat-варианте; Ф.1 AHR → «без НДС»,
    Ф.1 OPER/CAP → «с учётом НДС», все листы Ф.4 → «без НДС»; после рендера
    не осталось {vat}/{vat_plan}/{unit}; SMETA_COLUMNS — «тыс. руб.»
    у всех денежных меток и не у section_code/name). 2 оставшихся
    failed в test_export_service_query_params.py
    (ignore_non_applicable_query_params vs ignore_unsupported_sheet_params)
    — предсуществующие, не связаны с этой веткой, не трогал.

Rulings

  • Ф.1 AHR / Ф.4 VAT-wording: экран для Ф.1 AHR показывает «без НДС» на
    под-колонках при противоречащем групповом заголовке «с учётом складского
    НДС» (который тикет просил не трогать, вопрос 1 открыт). Для экспорта
    выбрал «без НДС» как финальную формулировку для Ф.1 AHR и всех листов
    Ф.4 — по явному дефолту тикета («соответствует экрану»), раз группа
    {vat}/{vat_plan} общая для OPER/AHR/CAP в FORM1_DEPTH_COLUMNS.
  • SMETA_COLUMNS_BY_FORM: на этой ветке (стек test→#288) этого символа в
    export_mappings.py нет — AURORA-1903/#287 с ним не влит и не стекуется
    сюда. Использовал SMETA_COLUMNS, как и предполагал тикет на этот случай.
  • «Планирование годовых показателей, {unit} (без НДС)» — применил
    только к этой ровно named группе (Ф.2 OPER/AHR/CAP), не к остальным
    денежным под-группам тех же листов (корректировки/бронь/перенос/
    секвестр и т.п.) — тикет привёл это как пример («и аналогично для
    остальных денежных групп»), но лист OPER/AHR/CAP для Ф.2 содержит
    десятки таких под-групп без явного списка от продукта; расширять
    трактовку без входа от заказчика не стал, чтобы не гадать за продукт.
  • Ф.1/Ф.4 SMETA «топ-группа»: тикет говорил про «top header group»
    (единственное число); применил , {unit} к каждой из топ-групп листа
    (План/Утверждено/Факт/Скорректировано) — иначе на экране остались бы
    непомеченные суммы по остальным стадиям.
  • PROJECT (форма 3, «Проекты») — только экран (три группы в
    CURRENT_EXPENSES.js/LIMIT.js); экспорт формы 3 (FORM3_CURRENT_EXPENSES_ DEPTH_COLUMNS/FORM3_LIMIT_DEPTH_COLUMNS, export_project_report_to_xlsx)
    не трогал — не упомянут в «Где»/«Что сделать» тикета, у него нет
    метаданных-строки с единицей сейчас и это не часть явного списка правок.
  • «Единица: тыс. руб.» — добавил только в export_summary_payload
    (Свод 1/2). export_project_report_to_xlsx (форма 3) не трогал — по
    той же причине, что выше.
  • web/package-lock.json: npm install в этом воркспейсе шёл не через
    корпоративный Nexus (недоступен из песочницы — 302 на страницу логина),
    а через публичный registry.npmjs.org (доступен). Lockfile после установки
    чуть отличался (убрана опциональная transitive-зависимость yaml под
    tailwindcss); откатил package-lock.json перед коммитом, чтобы не
    тащить в диф посторонний дрейф.
Closes #289. ## Стек веток (важно для ревью) Эта ветка стоит не на test, а поверх PR #288 (AURORA-1899, модуль `web/src/components/RealtimeTable/utils/moneyUtils.js` с `getMoneyUnit`/ `getDisplayFactor`/`UNIT_IN_RUB`/`AHR_LIMIT_SHEET`, переписанный `useDenomination.js` с дефолтом `'thousand'` и ключом `_denomination_v2`, `displayFactor` в `tableColumns.jsx`/`Cell.jsx`). test → PR #288 (AURORA-1899) → эта ветка (коммит 24ca441). Диф этого PR **временно включает коммит #288** (он ещё не в test). PR создан с `--base test` по процессу; после мерджа #288 ревьюеру будет проще смотреть только коммит 24ca441. ## Проблема Единица и учёт НДС в заголовках денежных колонок подписаны по-разному на разных формах, где-то не подписаны вовсе. Выгрузка в Excel расходится с экраном: `export_mappings.py` всегда писал «тыс. руб.» (денежные значения в базе хранятся в тыс. руб., денежный селектор на экран не влияет на выгрузку), при этом в 96 местах — с опечаткой «с учетом НДC» (латинская C). ## Что сделано ### Экран - `formatHeaderUnit(header, denomination)` (headerYearUtils.js, рядом с `formatHeaderYear`) — подставляет в заголовок плейсхолдер `{unit}` по разрядности: «руб.», «тыс. руб.», «млн руб.», «млрд руб.». Переиспользует `resolveDenomination`/`UNIT_IN_RUB` из AURORA-1899, не дублирует маппинг единиц. Без переданной разрядности — литерал «тыс. руб.» (дефолт). - `TableHead.jsx`: `formatHeaderUnit` вызывается сразу после `formatHeaderYear`, разрядность приходит новым пропом `denomination` (проброшен из `RealtimeTable.jsx`). - `CollapsibleTree.jsx`, `templateColumns.js` (шаблоны этапов): литерал «тыс. руб.» — там нет живого контекста разрядности. - `{unit}` вместо «тыс. руб.» в заголовках FORM_1 (OPER/AHR/CAP/ AHR_P_NO_LOCAL/CAP_NO_LOCAL), FORM_4 (AHR/CAP/OPER/STRUCTURE) — скриптовый проход + ручная проверка диффа. `AHR_LIMIT.js` **не трогали** — рублёвое исключение AURORA-1899/#288. - Добавили единицу, где её не было: FORM_1/FORM_4 SMETA (топ-группы «Расходы, планируемые...», «Итого», «Утверждённые...», «Фактические...», «Скорректированная...» → `, {unit}`); FORM_2 OPER/AHR/CAP («Планирование годовых показателей» → `, {unit} (без НДС)`); FORM_2 SMETA («В тыс. рублей» → «В {unit} (без НДС)»); FORM_2 АХР аренда/коммуналка/охрана (план «Планируемая сумма расходов на {year} год» → `, {unit}`; факт «В тыс. рублей» → `{unit}`); FORM_2 OTCH9F («Сумма (тыс. руб.)» → «Сумма ({unit})», «Тыс. руб.» → `{unit}`); PROJECT CURRENT_EXPENSES/LIMIT (три группы «...по ${planTitle}» → `, {unit}`). - Свод 1 (`SvodPage.jsx` + `SummaryTable.jsx`): метки `METRICS`/ `FORM_3_METRIC_LABELS` получили `, {unit}`; `scaleColumns` в `SummaryTable.jsx` теперь прогоняет заголовки через `formatHeaderUnit` с живой разрядностью (раньше трогал только значения ячеек). - Свод 2 (`Svod2Page.jsx`): «тыс. руб.» → `{unit}` в трёх заголовках (payment_amount, contract.ceiling, contract.expenses_qN); `scaleNumericColumns` аналогично прогоняет заголовки через `formatHeaderUnit`. - НДС-формулировки на экране **не менял** — вопрос 1 из тикета оставлен как есть (default: экран не трогаем, выгрузку подгоняем под экран). ### Выгрузка (`export_mappings.py` / `export_service.py`) - `FORM1_DEPTH_COLUMNS`: «с учетом НДC» (латинская C, 96 мест) → плейсхолдер `{vat}`; «План {year}(тыс. руб.) с учетом НДС» → «План {year} (тыс. руб.) {vat_plan}» (плейсхолдер только для НДС-части — единица в выгрузке всегда литерал «тыс. руб.», разрядность не экспортируется). Правка этого словаря автоматически применяется к `FORM4_DEPTH_COLUMNS`/ `FORM4_STRUCTURE_DEPTH_COLUMNS`/`FORM4_OPER_DEPTH_COLUMNS` — они строятся из него через `_rename_depth_columns`/`copy.deepcopy` при загрузке модуля. - `render_column_years(columns, year, extra=None)`: необязательный `extra` — дополнительные плейсхолдеры (например `{vat}`/`{vat_plan}`), резолвятся той же функцией `_replace_column_year_placeholders`, что и `{year}`. - `_create_xlsxwriter_sheet_writers`: по листу передаёт нужный `extra` — Ф.1 OPER/CAP → «с учётом НДС»; Ф.1 AHR и все листы Ф.4 (OPER/AHR/CAP/ STRUCTURE) → «без НДС» (соответствует экрану). - `SMETA_COLUMNS`: ко всем денежным меткам (кроме `section_code`/`name`) добавлено «, тыс. руб.». - Своды (`export_summary_payload`): в строку метаданных добавлена `("Единица", "тыс. руб.")`. ## Тесты - `node --test src/components/RealtimeTable/utils/*.test.js` — **55/55 зелёных** (51 существовавших + 4 новых на `formatHeaderUnit`: подстановка по разрядности, заголовок без плейсхолдера не меняется, дефолт на `undefined`/`null`/`''`). - `npm run build` — без ошибок. - `pytest tests/unit` (venv с зависимостями из `requirements-dev.txt`) — **102 passed**, 15 из них новые в `tests/unit/test_export_mappings_labels.py` (нет латинской C в НДС ни при каком `vat`-варианте; Ф.1 AHR → «без НДС», Ф.1 OPER/CAP → «с учётом НДС», все листы Ф.4 → «без НДС»; после рендера не осталось `{vat}`/`{vat_plan}`/`{unit}`; `SMETA_COLUMNS` — «тыс. руб.» у всех денежных меток и не у `section_code`/`name`). 2 оставшихся failed в `test_export_service_query_params.py` (`ignore_non_applicable_query_params` vs `ignore_unsupported_sheet_params`) — **предсуществующие**, не связаны с этой веткой, не трогал. ## Rulings - **Ф.1 AHR / Ф.4 VAT-wording**: экран для Ф.1 AHR показывает «без НДС» на под-колонках при противоречащем групповом заголовке «с учётом складского НДС» (который тикет просил не трогать, вопрос 1 открыт). Для экспорта выбрал «без НДС» как финальную формулировку для Ф.1 AHR и всех листов Ф.4 — по явному дефолту тикета («соответствует экрану»), раз группа `{vat}`/`{vat_plan}` общая для OPER/AHR/CAP в `FORM1_DEPTH_COLUMNS`. - **SMETA_COLUMNS_BY_FORM**: на этой ветке (стек test→#288) этого символа в `export_mappings.py` нет — AURORA-1903/#287 с ним не влит и не стекуется сюда. Использовал `SMETA_COLUMNS`, как и предполагал тикет на этот случай. - **«Планирование годовых показателей, {unit} (без НДС)»** — применил только к этой ровно named группе (Ф.2 OPER/AHR/CAP), не к остальным денежным под-группам тех же листов (корректировки/бронь/перенос/ секвестр и т.п.) — тикет привёл это как пример («и аналогично для остальных денежных групп»), но лист OPER/AHR/CAP для Ф.2 содержит десятки таких под-групп без явного списка от продукта; расширять трактовку без входа от заказчика не стал, чтобы не гадать за продукт. - **Ф.1/Ф.4 SMETA «топ-группа»**: тикет говорил про «top header group» (единственное число); применил `, {unit}` к каждой из топ-групп листа (План/Утверждено/Факт/Скорректировано) — иначе на экране остались бы непомеченные суммы по остальным стадиям. - **PROJECT (форма 3, «Проекты»)** — только экран (три группы в CURRENT_EXPENSES.js/LIMIT.js); экспорт формы 3 (`FORM3_CURRENT_EXPENSES_ DEPTH_COLUMNS`/`FORM3_LIMIT_DEPTH_COLUMNS`, `export_project_report_to_xlsx`) не трогал — не упомянут в «Где»/«Что сделать» тикета, у него нет метаданных-строки с единицей сейчас и это не часть явного списка правок. - **«Единица: тыс. руб.»** — добавил только в `export_summary_payload` (Свод 1/2). `export_project_report_to_xlsx` (форма 3) не трогал — по той же причине, что выше. - **web/package-lock.json**: `npm install` в этом воркспейсе шёл не через корпоративный Nexus (недоступен из песочницы — 302 на страницу логина), а через публичный registry.npmjs.org (доступен). Lockfile после установки чуть отличался (убрана опциональная transitive-зависимость `yaml` под `tailwindcss`); откатил `package-lock.json` перед коммитом, чтобы не тащить в диф посторонний дрейф.
aep added 2 commits 2026-11-01 19:19:11 +03:00
Селектор разрядности (AURORA-1650) считал базой рубли, хотя в базе суммы
хранятся в тыс. руб. (формы 1/4, сателлиты формы 2, лимитные строки АХР
после AURORA-1897). По умолчанию стояло «в руб.», и тысячи показывались
под видом рублей; при выборе «в тыс. руб.» значения делились ещё раз.
Делитель также применялся к количествам, площади, ставке НДС, курсу,
кодам и ID — не только к деньгам. Редактор ячейки не учитывал разрядность.

- web/src/components/RealtimeTable/utils/moneyUtils.js — новый модуль:
  UNIT_IN_RUB, getDisplayFactor(baseUnit, denomination), getMoneyUnit
  (денежная единица колонки по accessorKey/sheetName, с явным
  columnDef.moneyUnit как переопределением), TEXT_COLUMN_IDS (перенесён
  из Cell.jsx), resolveDenomination/DEFAULT_DENOMINATION, AHR_LIMIT_SHEET.
  Модуль не импортирует конфиги форм — работает под node --test.
- useDenomination.js — значение по умолчанию 'thousand', ключ хранения
  со суффиксом _denomination_v2 (старые 'rub'/'thousand' больше не
  читаются со старым смыслом); getDenominationDivisor убран.
- tableColumns.jsx/Cell.jsx — displayDivisor заменён на displayFactor
  (умножение, не деление), множитель считается на колонку через
  getMoneyUnit/getDisplayFactor с учётом sheetName; isEditable запрещает
  редактирование денежной ячейки, если разрядность не базовая; на листе
  AHR_LIMIT множитель всегда 1 (значения в рублях, как есть).
- SettingPanel.jsx — селектор скрыт на листе AHR_LIMIT, рядом с ним
  подсказка «Ввод сумм — в тыс. руб.».
- SvodPage/Svod2Page/ProjectSummariesPage — масштабирование по
  meta.money вместо regex по id/блок-листа текстовых id; построители
  колонок проставляют meta.money на денежных метриках.
- DenominationSelect.jsx — «в млн руб.», «в млрд руб.» без точки.

Тесты: web/src/components/RealtimeTable/utils/moneyUtils.test.js
(node --test), полный набор web/src/components/RealtimeTable/utils —
52/52 зелёных. npm run build проходит.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Экран: {unit}-плейсхолдер (formatHeaderUnit, переиспользует разрядность из
useDenomination/moneyUtils AURORA-1899) в TableHead.jsx, CollapsibleTree.jsx,
templateColumns.js; литерал «тыс. руб.» → {unit} в заголовках FORM_1/FORM_2/
FORM_4/PROJECT (кроме AHR_LIMIT.js — рублёвое исключение не трогаем) и в
своих колонках Свод1/Свод2 (SummaryTable.scaleColumns, Svod2Page.
scaleNumericColumns). Текст НДС на экране не менялся.

Выгрузка: «с учетом НДC» (латинская C, 96 мест в FORM1_DEPTH_COLUMNS и
производных FORM4_*) → плейсхолдер {vat}/{vat_plan}, резолвится по листу
через render_column_years(..., extra=...) — Ф.1 OPER/CAP «с учётом НДС»,
Ф.1 AHR и все листы Ф.4 «без НДС» (соответствует экрану). SMETA_COLUMNS —
«, тыс. руб.» у сумм. Своды — строка метаданных «Единица: тыс. руб.».

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
tsygankoviva approved these changes 2026-11-02 11:16:27 +03:00
aep added 1 commit 2026-11-02 20:48:15 +03:00
# Conflicts:
#	web/src/components/RealtimeTable/constants/FORM_1/SMETA.js
aep merged commit d12fa0f585 into test 2026-11-02 20:48:37 +03:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RSHB/DFiP_Budget_planing#291
No description provided.