From 24ca4410f451fedb7788b886c01e8fcf0f8ca518 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 1 Nov 2026 20:16:27 +0400 Subject: [PATCH] =?UTF-8?q?feat(labels):=20=D0=B5=D0=B4=D0=B8=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=B8=20=C2=AB?= =?UTF-8?q?=D1=82=D1=8B=D1=81.=20=D1=80=D1=83=D0=B1.=C2=BB=20=D0=B8=20?= =?UTF-8?q?=D0=9D=D0=94=D0=A1=20=D0=BD=D0=B0=20=D1=8D=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=B8=20=D0=B2=20=D0=B2=D1=8B=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=B7=D0=BA=D0=B5=20(AURORA-1900)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Экран: {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 --- .../export_service/export_mappings.py | 284 +++++++++--------- .../services/export_service/export_service.py | 53 +++- api/tests/unit/test_export_mappings_labels.py | 112 +++++++ .../RealtimeTable/RealtimeTable.jsx | 1 + .../RealtimeTable/TableHead/TableHead.jsx | 11 +- .../RealtimeTable/constants/FORM_1/AHR.js | 172 +++++------ .../constants/FORM_1/AHR_P_NO_LOCAL.js | 178 +++++------ .../RealtimeTable/constants/FORM_1/CAP.js | 172 +++++------ .../constants/FORM_1/CAP_NO_LOCAL.js | 178 +++++------ .../RealtimeTable/constants/FORM_1/OPER.js | 172 +++++------ .../RealtimeTable/constants/FORM_1/SMETA.js | 10 +- .../RealtimeTable/constants/FORM_2/AHR.js | 2 +- .../constants/FORM_2/AHR_RENT.js | 34 +-- .../constants/FORM_2/AHR_SECURITY.js | 34 +-- .../constants/FORM_2/AHR_UTILITY.js | 34 +-- .../RealtimeTable/constants/FORM_2/CAP.js | 2 +- .../RealtimeTable/constants/FORM_2/OPER.js | 2 +- .../RealtimeTable/constants/FORM_2/OTCH9F.js | 20 +- .../RealtimeTable/constants/FORM_2/SMETA.js | 2 +- .../RealtimeTable/constants/FORM_4/AHR.js | 22 +- .../RealtimeTable/constants/FORM_4/CAP.js | 22 +- .../RealtimeTable/constants/FORM_4/OPER.js | 22 +- .../RealtimeTable/constants/FORM_4/SMETA.js | 8 +- .../constants/FORM_4/STRUCTURE.js | 168 +++++------ .../constants/PROJECT/CURRENT_EXPENSES.js | 6 +- .../RealtimeTable/constants/PROJECT/LIMIT.js | 6 +- .../RealtimeTable/utils/headerYearUtils.js | 25 ++ .../utils/headerYearUtils.test.js | 21 +- web/src/components/common/CollapsibleTree.jsx | 6 +- .../utils/templateColumns.js | 4 +- web/src/pages/Svod2Page/Svod2Page.jsx | 34 ++- web/src/pages/SvodPage/SummaryTable.jsx | 47 +-- web/src/pages/SvodPage/SvodPage.jsx | 16 +- 33 files changed, 1043 insertions(+), 837 deletions(-) create mode 100644 api/tests/unit/test_export_mappings_labels.py diff --git a/api/src/services/export_service/export_mappings.py b/api/src/services/export_service/export_mappings.py index 2398a18..dfce210 100644 --- a/api/src/services/export_service/export_mappings.py +++ b/api/src/services/export_service/export_mappings.py @@ -49,7 +49,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "plan": { - "name": "План {year}(тыс. руб.) с учетом НДС", + "name": "План {year} (тыс. руб.) {vat_plan}", "children": { "q1": { "name": "1 кв. {year_short}", @@ -200,7 +200,7 @@ FORM1_DEPTH_COLUMNS: dict = { "name": "Коллегиальные органы", "children": { "approved": { - "name": "Сумма (с учетом НДC)", + "name": "Сумма ({vat})", }, "protocol": { "name": "Реквизиты протокола (дата, номер)", @@ -217,19 +217,19 @@ FORM1_DEPTH_COLUMNS: dict = { "name": "ЦКК", "children": { "ceiling": { - "name": "Предельная ст-ть, тыс. руб. (с учетом НДC)", + "name": "Предельная ст-ть, тыс. руб. ({vat})", }, "q1": { - "name": "Расходы 1 кв. {year} г. тыс. руб. (с учетом НДC)", + "name": "Расходы 1 кв. {year} г. тыс. руб. ({vat})", }, "q2": { - "name": "Расходы 2 кв. {year} г. тыс. руб. (с учетом НДC)", + "name": "Расходы 2 кв. {year} г. тыс. руб. ({vat})", }, "q3": { - "name": "Расходы 3 кв. {year} г. тыс. руб. (с учетом НДC)", + "name": "Расходы 3 кв. {year} г. тыс. руб. ({vat})", }, "q4": { - "name": "Расходы 4 кв. {year} г. тыс. руб. (с учетом НДC)", + "name": "Расходы 4 кв. {year} г. тыс. руб. ({vat})", }, "rf_schedule": { "name": "по сметам РФ (1 кв, 2 кв, 3 кв, 4 кв)", @@ -267,19 +267,19 @@ FORM1_DEPTH_COLUMNS: dict = { "name": "Валюта договора", }, "ceiling": { - "name": "Предельная ст-ть, тыс. руб. (с учетом НДC)", + "name": "Предельная ст-ть, тыс. руб. ({vat})", }, "q1": { - "name": "Расходы 1 кв. {year} г. тыс. руб. (с учетом НДC)", + "name": "Расходы 1 кв. {year} г. тыс. руб. ({vat})", }, "q2": { - "name": "Расходы 2 кв. {year} г. тыс. руб. (с учетом НДC)", + "name": "Расходы 2 кв. {year} г. тыс. руб. ({vat})", }, "q3": { - "name": "Расходы 3 кв. {year} г. тыс. руб. (с учетом НДC)", + "name": "Расходы 3 кв. {year} г. тыс. руб. ({vat})", }, "q4": { - "name": "Расходы 4 кв. {year} г. тыс. руб. (с учетом НДC)", + "name": "Расходы 4 кв. {year} г. тыс. руб. ({vat})", }, "rf_schedule": { "name": "по сметам РФ (1 кв, 2 кв, 3 кв, 4 кв)", @@ -291,7 +291,7 @@ FORM1_DEPTH_COLUMNS: dict = { "name": "Курс пересчета", }, "amount_foreign": { - "name": "Сумма (с учетом НДC), вал (с учетом НДC)", + "name": "Сумма ({vat}), вал ({vat})", }, "deadline": { "name": "Срок", @@ -321,7 +321,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "adj_current": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -330,7 +330,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "adj_ssp": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -339,7 +339,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "adj_rf": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -348,7 +348,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "adj_reserve": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -357,7 +357,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "sum": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -366,7 +366,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "adj_comment": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -375,7 +375,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "corrected_plan": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -390,13 +390,13 @@ FORM1_DEPTH_COLUMNS: dict = { "name": "Дата платежа" }, "pay_amount": { - "name": "Сумма, тыс. руб. (с учетом НДC)" + "name": "Сумма, тыс. руб. ({vat})" }, "pay_ho": { - "name": "в т.ч. сумма (с учетом НДC) ГО" + "name": "в т.ч. сумма ({vat}) ГО" }, "pay_rf": { - "name": "в т.ч. сумма (с учетом НДC) РФ" + "name": "в т.ч. сумма ({vat}) РФ" }, "pay_comment": { "name": "Комментарий" @@ -414,7 +414,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "booking": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -423,7 +423,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "actual_m1": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -432,7 +432,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "actual_m2": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -441,7 +441,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "actual_m3": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -450,7 +450,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "actual_quarter": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -459,7 +459,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rem_booking": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -468,7 +468,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rem_actual": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -505,7 +505,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rev_eco": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -514,7 +514,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rev_item": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -523,7 +523,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rev_inc": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -532,7 +532,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rev_seq": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -541,7 +541,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rev_comment": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -550,7 +550,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "new_plan": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -564,7 +564,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "adj_current": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -573,7 +573,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "adj_ssp": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -582,7 +582,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "adj_rf": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -591,7 +591,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "adj_reserve": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -600,7 +600,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "sum": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -609,7 +609,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "corrected_plan": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -624,13 +624,13 @@ FORM1_DEPTH_COLUMNS: dict = { "name": "Дата платежа" }, "pay_amount": { - "name": "Сумма, тыс. руб. (с учетом НДC)" + "name": "Сумма, тыс. руб. ({vat})" }, "pay_ho": { - "name": "в т.ч. сумма (с учетом НДC) ГО" + "name": "в т.ч. сумма ({vat}) ГО" }, "pay_rf": { - "name": "в т.ч. сумма (с учетом НДC) РФ" + "name": "в т.ч. сумма ({vat}) РФ" }, "pay_comment": { "name": "Комментарий" @@ -645,7 +645,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "booking": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -654,7 +654,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "actual_m1": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -663,7 +663,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "actual_m2": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -672,7 +672,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "actual_m3": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -681,7 +681,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "actual_quarter": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -690,7 +690,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rem_booking": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -699,7 +699,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rem_actual": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -732,7 +732,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rev_eco": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -741,7 +741,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rev_item": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -750,7 +750,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rev_inc": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -759,7 +759,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rev_seq": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -768,7 +768,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rev_comment": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -777,7 +777,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "new_plan": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -791,7 +791,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "adj_current": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -800,7 +800,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "adj_ssp": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -809,7 +809,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "adj_rf": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -818,7 +818,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "adj_reserve": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -827,7 +827,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "sum": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -836,7 +836,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "corrected_plan": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -851,13 +851,13 @@ FORM1_DEPTH_COLUMNS: dict = { "name": "Дата платежа" }, "pay_amount": { - "name": "Сумма, тыс. руб. (с учетом НДC)" + "name": "Сумма, тыс. руб. ({vat})" }, "pay_ho": { - "name": "в т.ч. сумма (с учетом НДC) ГО" + "name": "в т.ч. сумма ({vat}) ГО" }, "pay_rf": { - "name": "в т.ч. сумма (с учетом НДC) РФ" + "name": "в т.ч. сумма ({vat}) РФ" }, "pay_comment": { "name": "Комментарий" @@ -872,7 +872,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "booking": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -881,7 +881,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "actual_m1": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -890,7 +890,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "actual_m2": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -899,7 +899,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "actual_m3": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -908,7 +908,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "actual_quarter": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -917,7 +917,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rem_booking": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -926,7 +926,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rem_actual": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -956,7 +956,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rev_eco": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -965,7 +965,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rev_item": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -974,7 +974,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rev_inc": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -983,7 +983,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rev_seq": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -992,10 +992,10 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rev_comment": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", }, "adj_comment": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", }, } }, @@ -1004,7 +1004,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "new_plan": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -1018,7 +1018,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "adj_current": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -1027,7 +1027,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "adj_ssp": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -1036,7 +1036,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "adj_rf": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -1045,7 +1045,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "adj_reserve": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -1054,7 +1054,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "corrected_plan": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -1069,13 +1069,13 @@ FORM1_DEPTH_COLUMNS: dict = { "name": "Дата платежа" }, "pay_amount": { - "name": "Сумма, тыс. руб. (с учетом НДC)" + "name": "Сумма, тыс. руб. ({vat})" }, "pay_ho": { - "name": "в т.ч. сумма (с учетом НДC) ГО" + "name": "в т.ч. сумма ({vat}) ГО" }, "pay_rf": { - "name": "в т.ч. сумма (с учетом НДC) РФ" + "name": "в т.ч. сумма ({vat}) РФ" }, "pay_comment": { "name": "Комментарий" @@ -1090,7 +1090,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "booking": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -1099,7 +1099,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "actual_m1": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -1108,7 +1108,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "actual_m2": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -1117,7 +1117,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "actual_m3": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -1126,7 +1126,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "actual_quarter": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -1135,7 +1135,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "actual_spod": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -1144,7 +1144,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rem_booking": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -1153,7 +1153,7 @@ FORM1_DEPTH_COLUMNS: dict = { "key": None, "children": { "rem_actual": { - "name": "Сумма, тыс. руб. (с учетом НДC)", + "name": "Сумма, тыс. руб. ({vat})", } } }, @@ -3389,45 +3389,45 @@ FORM4_OPER_COLORS: dict = _rename_color_paths( SMETA_COLUMNS: list[tuple[str, str]] = [ ("section_code", "Код раздела"), ("name", "Наименование"), - ("plan_support_q1", "План поддержка I квартал"), - ("plan_support_q2", "План поддержка II квартал"), - ("plan_support_q3", "План поддержка III квартал"), - ("plan_support_q4", "План поддержка IV квартал"), - ("plan_support_year", "План поддержка Год"), - ("plan_development_q1", "План развитие I квартал"), - ("plan_development_q2", "План развитие II квартал"), - ("plan_development_q3", "План развитие III квартал"), - ("plan_development_q4", "План развитие IV квартал"), - ("plan_development_year", "План развитие Год"), - ("plan_total_year", "План ИТОГО Год"), - ("approved_support_q1", "Утверждено поддержка I квартал"), - ("approved_support_q2", "Утверждено поддержка II квартал"), - ("approved_support_q3", "Утверждено поддержка III квартал"), - ("approved_support_q4", "Утверждено поддержка IV квартал"), - ("approved_support_year", "Утверждено поддержка Год"), - ("approved_development_q1", "Утверждено развитие I квартал"), - ("approved_development_q2", "Утверждено развитие II квартал"), - ("approved_development_q3", "Утверждено развитие III квартал"), - ("approved_development_q4", "Утверждено развитие IV квартал"), - ("approved_development_year", "Утверждено развитие Год"), - ("approved_total_year", "Утверждено ИТОГО Год"), - ("fact_support_q1", "Факт поддержка I квартал"), - ("fact_support_q2", "Факт поддержка II квартал"), - ("fact_support_q3", "Факт поддержка III квартал"), - ("fact_support_q4", "Факт поддержка IV квартал"), - ("fact_support_year", "Факт поддержка Год"), - ("fact_development_q1", "Факт развитие I квартал"), - ("fact_development_q2", "Факт развитие II квартал"), - ("fact_development_q3", "Факт развитие III квартал"), - ("fact_development_q4", "Факт развитие IV квартал"), - ("fact_development_year", "Факт развитие Год"), - ("fact_total_year", "Факт ИТОГО Год"), - ("corrected_support_q2", "Корректировка поддержка II квартал"), - ("corrected_support_q3", "Корректировка поддержка III квартал"), - ("corrected_support_q4", "Корректировка поддержка IV квартал"), - ("corrected_development_q2", "Корректировка развитие II квартал"), - ("corrected_development_q3", "Корректировка развитие III квартал"), - ("corrected_development_q4", "Корректировка развитие IV квартал"), + ("plan_support_q1", "План поддержка I квартал, тыс. руб."), + ("plan_support_q2", "План поддержка II квартал, тыс. руб."), + ("plan_support_q3", "План поддержка III квартал, тыс. руб."), + ("plan_support_q4", "План поддержка IV квартал, тыс. руб."), + ("plan_support_year", "План поддержка Год, тыс. руб."), + ("plan_development_q1", "План развитие I квартал, тыс. руб."), + ("plan_development_q2", "План развитие II квартал, тыс. руб."), + ("plan_development_q3", "План развитие III квартал, тыс. руб."), + ("plan_development_q4", "План развитие IV квартал, тыс. руб."), + ("plan_development_year", "План развитие Год, тыс. руб."), + ("plan_total_year", "План ИТОГО Год, тыс. руб."), + ("approved_support_q1", "Утверждено поддержка I квартал, тыс. руб."), + ("approved_support_q2", "Утверждено поддержка II квартал, тыс. руб."), + ("approved_support_q3", "Утверждено поддержка III квартал, тыс. руб."), + ("approved_support_q4", "Утверждено поддержка IV квартал, тыс. руб."), + ("approved_support_year", "Утверждено поддержка Год, тыс. руб."), + ("approved_development_q1", "Утверждено развитие I квартал, тыс. руб."), + ("approved_development_q2", "Утверждено развитие II квартал, тыс. руб."), + ("approved_development_q3", "Утверждено развитие III квартал, тыс. руб."), + ("approved_development_q4", "Утверждено развитие IV квартал, тыс. руб."), + ("approved_development_year", "Утверждено развитие Год, тыс. руб."), + ("approved_total_year", "Утверждено ИТОГО Год, тыс. руб."), + ("fact_support_q1", "Факт поддержка I квартал, тыс. руб."), + ("fact_support_q2", "Факт поддержка II квартал, тыс. руб."), + ("fact_support_q3", "Факт поддержка III квартал, тыс. руб."), + ("fact_support_q4", "Факт поддержка IV квартал, тыс. руб."), + ("fact_support_year", "Факт поддержка Год, тыс. руб."), + ("fact_development_q1", "Факт развитие I квартал, тыс. руб."), + ("fact_development_q2", "Факт развитие II квартал, тыс. руб."), + ("fact_development_q3", "Факт развитие III квартал, тыс. руб."), + ("fact_development_q4", "Факт развитие IV квартал, тыс. руб."), + ("fact_development_year", "Факт развитие Год, тыс. руб."), + ("fact_total_year", "Факт ИТОГО Год, тыс. руб."), + ("corrected_support_q2", "Корректировка поддержка II квартал, тыс. руб."), + ("corrected_support_q3", "Корректировка поддержка III квартал, тыс. руб."), + ("corrected_support_q4", "Корректировка поддержка IV квартал, тыс. руб."), + ("corrected_development_q2", "Корректировка развитие II квартал, тыс. руб."), + ("corrected_development_q3", "Корректировка развитие III квартал, тыс. руб."), + ("corrected_development_q4", "Корректировка развитие IV квартал, тыс. руб."), ] FORM3_LIMIT_DEPTH_COLUMNS: dict = { "header": { @@ -3996,8 +3996,14 @@ def _replace_column_year_placeholders(columns: dict, year_values: dict[str, str] _replace_column_year_placeholders(column["children"], year_values) -def render_column_years(columns: dict, year: int) -> dict: - """Resolve year placeholders in header names without changing shared templates.""" +def render_column_years(columns: dict, year: int, extra: dict[str, str] | None = None) -> dict: + """Resolve year (и, опционально, прочие) плейсхолдеры в заголовках без изменения + общих шаблонов (FORM1_DEPTH_COLUMNS и производные от него делят один и тот же + словарь между листами Ф.1/Ф.4 — рендерить нужно отдельную копию на каждый лист). + + `extra` — дополнительные плейсхолдеры (например {vat}/{vat_plan} для текста НДС, + AURORA-1900), которые для разных листов резолвятся в разный текст. + """ rendered = copy.deepcopy(columns) values = { "year": str(year), @@ -4005,6 +4011,8 @@ def render_column_years(columns: dict, year: int) -> dict: "next_year": str(year + 1), "next_year_short": f"{(year + 1) % 100:02d}", } + if extra: + values.update(extra) _replace_column_year_placeholders(rendered, values) return rendered diff --git a/api/src/services/export_service/export_service.py b/api/src/services/export_service/export_service.py index 00fe243..98dbba8 100644 --- a/api/src/services/export_service/export_service.py +++ b/api/src/services/export_service/export_service.py @@ -79,6 +79,19 @@ SHEET_NAMES = { 'CURRENT_EXPENSES': 'Текущие расходы', } SKIP_SHEETS = ('OTCH9F',) +# AURORA-1900: текст НДС в экспорте должен совпадать с экраном (вместо текстовой +# опечатки «с учетом НДC» — Latin C). Ф.1 AHR и все листы Ф.4 показывают суммы без НДС, +# Ф.1 OPER/CAP — с учётом НДС (см. render_column_years(..., extra=...) ниже). +FORM1_SHEET_VAT_WORDING = { + "OPER": "с учётом НДС", + "CAP": "с учётом НДС", + "AHR": "без НДС", +} +FORM4_SHEET_VAT_WORDING = "без НДС" + + +def _vat_placeholders(vat_phrase: str) -> dict[str, str]: + return {"vat": vat_phrase, "vat_plan": vat_phrase} BULK_EXPORT_CONCURRENCY = 2 EXPORT_TMPDIR = os.getenv("EXPORT_TMPDIR") or tempfile.gettempdir() @@ -251,7 +264,7 @@ class ExportService: stream = await self._export_summary_rows( rows=rows, columns=columns, - metadata=(("Свод", summary_number), ("Лист", sheet), ("Год", year)), + metadata=(("Свод", summary_number), ("Лист", sheet), ("Год", year), ("Единица", "тыс. руб.")), worksheet_name=f"Свод {summary_number} {sheet}", summary_number=summary_number, year=year, @@ -905,7 +918,9 @@ class ExportService: ) -> dict[str, Any]: default_writer = XlsxDepthSheetWriter( workbook=workbook, - columns_dict=render_column_years(FORM1_DEPTH_COLUMNS, year), + columns_dict=render_column_years( + FORM1_DEPTH_COLUMNS, year, _vat_placeholders(FORM1_SHEET_VAT_WORDING["OPER"]) + ), row_normalizer=self.form1_row_normalizer.normalize, value_serializer=self.value_serializer.to_excel_value, ) @@ -920,25 +935,33 @@ class ExportService: } if form_type_code == FormTypeEnum.FORM_4.value: writer_configs = { - "OPER": (FORM4_OPER_DEPTH_COLUMNS, FORM4_OPER_COLORS), - "AHR": (FORM4_DEPTH_COLUMNS, FORM4_COLORS["AHR"]), - "CAP": (FORM4_DEPTH_COLUMNS, FORM4_COLORS["CAP"]), - "STRUCTURE": (FORM4_STRUCTURE_DEPTH_COLUMNS, FORM4_STRUCTURE_COLORS), + "OPER": (FORM4_OPER_DEPTH_COLUMNS, FORM4_OPER_COLORS, _vat_placeholders(FORM4_SHEET_VAT_WORDING)), + "AHR": (FORM4_DEPTH_COLUMNS, FORM4_COLORS["AHR"], _vat_placeholders(FORM4_SHEET_VAT_WORDING)), + "CAP": (FORM4_DEPTH_COLUMNS, FORM4_COLORS["CAP"], _vat_placeholders(FORM4_SHEET_VAT_WORDING)), + "STRUCTURE": ( + FORM4_STRUCTURE_DEPTH_COLUMNS, + FORM4_STRUCTURE_COLORS, + _vat_placeholders(FORM4_SHEET_VAT_WORDING), + ), } else: writer_configs = { - "OPER": (FORM1_DEPTH_COLUMNS, FORM1_COLORS["OPER"]), - "AHR": (FORM1_DEPTH_COLUMNS, FORM1_COLORS["AHR"]), - "CAP": (FORM1_DEPTH_COLUMNS, FORM1_COLORS["CAP"]), - "AHR_RENT": (FORM2_DEPTH_AHR_SUB_COLUMNS, None), - "AHR_UTILITY": (FORM2_DEPTH_AHR_SUB_COLUMNS, None), - "AHR_SECURITY": (FORM2_DEPTH_AHR_SUB_COLUMNS, None), - "STRUCTURE": (FORM4_STRUCTURE_DEPTH_COLUMNS, FORM4_STRUCTURE_COLORS), + "OPER": (FORM1_DEPTH_COLUMNS, FORM1_COLORS["OPER"], _vat_placeholders(FORM1_SHEET_VAT_WORDING["OPER"])), + "AHR": (FORM1_DEPTH_COLUMNS, FORM1_COLORS["AHR"], _vat_placeholders(FORM1_SHEET_VAT_WORDING["AHR"])), + "CAP": (FORM1_DEPTH_COLUMNS, FORM1_COLORS["CAP"], _vat_placeholders(FORM1_SHEET_VAT_WORDING["CAP"])), + "AHR_RENT": (FORM2_DEPTH_AHR_SUB_COLUMNS, None, None), + "AHR_UTILITY": (FORM2_DEPTH_AHR_SUB_COLUMNS, None, None), + "AHR_SECURITY": (FORM2_DEPTH_AHR_SUB_COLUMNS, None, None), + "STRUCTURE": ( + FORM4_STRUCTURE_DEPTH_COLUMNS, + FORM4_STRUCTURE_COLORS, + _vat_placeholders(FORM4_SHEET_VAT_WORDING), + ), } - for sheet_name, (columns, colors) in writer_configs.items(): + for sheet_name, (columns, colors, extra) in writer_configs.items(): writers[sheet_name] = XlsxDepthSheetWriter( workbook=workbook, - columns_dict=render_column_years(columns, year), + columns_dict=render_column_years(columns, year, extra), row_normalizer=self.form1_row_normalizer.normalize, value_serializer=self.value_serializer.to_excel_value, colors_config=colors, diff --git a/api/tests/unit/test_export_mappings_labels.py b/api/tests/unit/test_export_mappings_labels.py new file mode 100644 index 0000000..e1f2b2e --- /dev/null +++ b/api/tests/unit/test_export_mappings_labels.py @@ -0,0 +1,112 @@ +"""AURORA-1900: единые подписи «тыс. руб.» и НДС в выгрузке Excel. + +Проверяет только текст меток (name) в FORM1_DEPTH_COLUMNS-производных мапах и в +SMETA_COLUMNS — набор и порядок колонок эта задача не трогает (AURORA-1902/1903/ +1904/1905). +""" + +import re + +import pytest + +from src.services.export_service.export_mappings import ( + FORM1_DEPTH_COLUMNS, + FORM4_DEPTH_COLUMNS, + FORM4_OPER_DEPTH_COLUMNS, + FORM4_STRUCTURE_DEPTH_COLUMNS, + SMETA_COLUMNS, + render_column_years, +) + +# Латинская C/c в «НДС» — опечатка AURORA-1900 (п.4 тикета), должна быть устранена везде. +LATIN_C_IN_NDS = re.compile(r"НД[Cc]\b|НД[Cc][^\wа-яё]", re.IGNORECASE) + +FORM1_SHEET_VAT = { + "OPER": "с учётом НДС", + "CAP": "с учётом НДС", + "AHR": "без НДС", +} +FORM4_SHEET_VAT = "без НДС" + +DEPTH_COLUMN_MAPPINGS = [ + FORM1_DEPTH_COLUMNS, + FORM4_DEPTH_COLUMNS, + FORM4_STRUCTURE_DEPTH_COLUMNS, + FORM4_OPER_DEPTH_COLUMNS, +] + + +def _collect_names(columns: dict) -> list[str]: + names = [] + for column in columns.values(): + if "name" in column: + names.append(column["name"]) + if "children" in column: + names.extend(_collect_names(column["children"])) + return names + + +def _rendered_names(columns: dict, vat_phrase: str) -> list[str]: + rendered = render_column_years(columns, 2026, {"vat": vat_phrase, "vat_plan": vat_phrase}) + return _collect_names(rendered) + + +def test_raw_templates_use_a_vat_placeholder_not_a_hardcoded_phrase(): + # Санити-чек: убедились, что типовой текст НДС стал плейсхолдером, а не был + # просто вычищен/захардкожен в другую фразу. + names = _collect_names(FORM1_DEPTH_COLUMNS) + assert any("{vat}" in name for name in names) + assert any("{vat_plan}" in name for name in names) + + +@pytest.mark.parametrize("columns", DEPTH_COLUMN_MAPPINGS) +def test_no_latin_c_typo_in_nds_for_any_sheet_vat_wording(columns): + for vat_phrase in (*FORM1_SHEET_VAT.values(), FORM4_SHEET_VAT): + for name in _rendered_names(columns, vat_phrase): + assert not LATIN_C_IN_NDS.search(name), f"Latin C in НДС: {name!r}" + + +@pytest.mark.parametrize("columns", DEPTH_COLUMN_MAPPINGS) +def test_no_leftover_placeholders_after_rendering(columns): + for vat_phrase in (*FORM1_SHEET_VAT.values(), FORM4_SHEET_VAT): + for name in _rendered_names(columns, vat_phrase): + assert "{vat}" not in name + assert "{vat_plan}" not in name + assert "{unit}" not in name + + +def test_form1_ahr_labels_say_without_vat_matching_screen(): + names = _rendered_names(FORM1_DEPTH_COLUMNS, FORM1_SHEET_VAT["AHR"]) + assert any("без НДС" in name for name in names) + assert not any("с учётом НДС" in name for name in names) + + +def test_form1_oper_labels_say_with_vat(): + names = _rendered_names(FORM1_DEPTH_COLUMNS, FORM1_SHEET_VAT["OPER"]) + assert any("с учётом НДС" in name for name in names) + assert not any(name == "без НДС" or "(без НДС)" in name for name in names) + + +def test_form1_cap_labels_say_with_vat(): + names = _rendered_names(FORM1_DEPTH_COLUMNS, FORM1_SHEET_VAT["CAP"]) + assert any("с учётом НДС" in name for name in names) + + +def test_form4_sheets_say_without_vat(): + for columns in (FORM4_DEPTH_COLUMNS, FORM4_STRUCTURE_DEPTH_COLUMNS, FORM4_OPER_DEPTH_COLUMNS): + names = _rendered_names(columns, FORM4_SHEET_VAT) + assert any("без НДС" in name for name in names) + + +def test_smeta_columns_have_thousand_rub_suffix_on_sum_labels(): + non_money_keys = {"section_code", "name"} + for key, label in SMETA_COLUMNS: + if key in non_money_keys: + assert "тыс. руб." not in label + continue + assert "тыс. руб." in label, f"{key} label missing тыс. руб.: {label!r}" + + +def test_smeta_columns_have_no_nds_typo(): + for _key, label in SMETA_COLUMNS: + assert not LATIN_C_IN_NDS.search(label), f"Latin C in НДС: {label!r}" diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx index 1d88ba2..fd7a244 100644 --- a/web/src/components/RealtimeTable/RealtimeTable.jsx +++ b/web/src/components/RealtimeTable/RealtimeTable.jsx @@ -805,6 +805,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { createPortal( { const pinningPosition = column.getIsPinned(); @@ -102,7 +102,7 @@ const getColorBrightness = (hexColor) => { return 0.299 * r + 0.587 * g + 0.114 * b; }; -const HeaderCell = ({ header, table, pinningPosition, onClick, onChangeWidth, budgetYear }) => { +const HeaderCell = ({ header, table, pinningPosition, onClick, onChangeWidth, budgetYear, denomination }) => { const column = header.column; const headerSize = header.getSize(); const isPinned = Boolean(pinningPosition); @@ -172,7 +172,7 @@ const HeaderCell = ({ header, table, pinningPosition, onClick, onChangeWidth, bu maxWidth: 'calc(100% - 1rem)', boxSizing: 'border-box', }}> - {header.isPlaceholder ? null : formatHeaderYear(column.columnDef.header, budgetYear)} + {header.isPlaceholder ? null : formatHeaderUnit(formatHeaderYear(column.columnDef.header, budgetYear), denomination)} @@ -317,7 +317,7 @@ const FilterCell = ({ column }) => { ); }; -const TableHeadComponent = ({ table, selectedColumnId, onColumnSelect, onChangeWidth, isLoadingData, budgetYear }) => { +const TableHeadComponent = ({ table, selectedColumnId, onColumnSelect, onChangeWidth, isLoadingData, budgetYear, denomination }) => { const handleHeaderClick = (header) => { if (!header.subHeaders?.length && header.column?.id) { onColumnSelect?.(header.column.id); @@ -355,6 +355,7 @@ const TableHeadComponent = ({ table, selectedColumnId, onColumnSelect, onChangeW (groups[rowIndex]?.headers || []).map((header) => ( { + if (typeof header !== 'string') return header; + if (!header.includes('{unit}')) return header; + const resolved = resolveDenomination(denomination); + const label = UNIT_LABELS[resolved] ?? UNIT_LABELS.thousand; + return header.replaceAll('{unit}', label); +}; + // {year}, {year+1}, {year+2}; :short keeps the two-digit year format. export const parseBudgetYear = (value) => { const year = Number(value); diff --git a/web/src/components/RealtimeTable/utils/headerYearUtils.test.js b/web/src/components/RealtimeTable/utils/headerYearUtils.test.js index deb5b2b..119519c 100644 --- a/web/src/components/RealtimeTable/utils/headerYearUtils.test.js +++ b/web/src/components/RealtimeTable/utils/headerYearUtils.test.js @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { formatHeaderYear, parseBudgetYear } from './headerYearUtils.js'; +import { formatHeaderUnit, formatHeaderYear, parseBudgetYear } from './headerYearUtils.js'; test('headers follow the estimate year and retain relative payment years', () => { const header = 'План {year}; платежи {year+1} и {year+2}'; @@ -24,3 +24,22 @@ test('ordinary labels and non-string headers are preserved', () => { assert.equal(formatHeaderYear('', 2026), ''); assert.equal(formatHeaderYear(null, 2026), null); }); + +test('formatHeaderUnit substitutes the {unit} placeholder for the selected denomination', () => { + assert.equal(formatHeaderUnit('Сумма, {unit} (без НДС)', 'million'), 'Сумма, млн руб. (без НДС)'); + assert.equal(formatHeaderUnit('План {year} ({unit}) с учётом НДС', 'billion'), 'План {year} (млрд руб.) с учётом НДС'); + assert.equal(formatHeaderUnit('{unit}', 'rub'), 'руб.'); + assert.equal(formatHeaderUnit('{unit}', 'thousand'), 'тыс. руб.'); +}); + +test('formatHeaderUnit leaves headers without the placeholder unchanged', () => { + assert.equal(formatHeaderUnit('Наименование', 'million'), 'Наименование'); + assert.equal(formatHeaderUnit('', 'million'), ''); + assert.equal(formatHeaderUnit(null, 'million'), null); +}); + +test('formatHeaderUnit defaults to thousand (literal "тыс. руб.") when no denomination is given', () => { + assert.equal(formatHeaderUnit('Сумма, {unit}', undefined), 'Сумма, тыс. руб.'); + assert.equal(formatHeaderUnit('Сумма, {unit}', null), 'Сумма, тыс. руб.'); + assert.equal(formatHeaderUnit('Сумма, {unit}', ''), 'Сумма, тыс. руб.'); +}); diff --git a/web/src/components/common/CollapsibleTree.jsx b/web/src/components/common/CollapsibleTree.jsx index 93ae504..339627f 100644 --- a/web/src/components/common/CollapsibleTree.jsx +++ b/web/src/components/common/CollapsibleTree.jsx @@ -4,7 +4,7 @@ import { Box, Checkbox, InputAdornment, Popover, TextField, Typography } from '@ import { SimpleTreeView, TreeItem } from '@mui/x-tree-view'; import { memo, useMemo, useState } from 'react'; import { filterColumnTree, getColumnKey, getColumnTreeLeafKeys } from '../RealtimeTable/utils/columnUtils'; -import { formatHeaderYear } from '../RealtimeTable/utils/headerYearUtils'; +import { formatHeaderUnit, formatHeaderYear } from '../RealtimeTable/utils/headerYearUtils'; const EMPTY_IDS = []; @@ -114,7 +114,9 @@ const CollapsibleTree = ({ checkedIcon={} indeterminateIcon={} /> - {formatHeaderYear(node.columnDef?.header || node.name || node.header || 'Без названия', budgetYear)} + + {formatHeaderUnit(formatHeaderYear(node.columnDef?.header || node.name || node.header || 'Без названия', budgetYear))} + }> {node.columns && node.columns.length > 0 && renderTree(node.columns)} diff --git a/web/src/pages/AdminPanel/PhaseTemplatesPage/utils/templateColumns.js b/web/src/pages/AdminPanel/PhaseTemplatesPage/utils/templateColumns.js index 5910d66..7a66571 100644 --- a/web/src/pages/AdminPanel/PhaseTemplatesPage/utils/templateColumns.js +++ b/web/src/pages/AdminPanel/PhaseTemplatesPage/utils/templateColumns.js @@ -1,5 +1,5 @@ import { filterColumnTree } from '../../../../components/RealtimeTable/utils/columnUtils'; -import { formatHeaderYear } from '../../../../components/RealtimeTable/utils/headerYearUtils'; +import { formatHeaderUnit, formatHeaderYear } from '../../../../components/RealtimeTable/utils/headerYearUtils'; export async function loadTemplateColumnTree(formType, sheet) { const configForm = formType === 'FORM_3' ? 'PROJECT' : formType; @@ -12,7 +12,7 @@ export async function loadTemplateColumns(formType, sheet, excludedKeys = []) { const options = []; const collect = (nodes, parents = []) => { for (const node of nodes) { - const header = typeof node.header === 'string' ? formatHeaderYear(node.header) : ''; + const header = typeof node.header === 'string' ? formatHeaderUnit(formatHeaderYear(node.header)) : ''; const path = [...parents, header].filter(Boolean); if (node.columns?.length) collect(node.columns, path); else if (node.accessorKey?.startsWith('data.')) diff --git a/web/src/pages/Svod2Page/Svod2Page.jsx b/web/src/pages/Svod2Page/Svod2Page.jsx index e4551f2..6870758 100644 --- a/web/src/pages/Svod2Page/Svod2Page.jsx +++ b/web/src/pages/Svod2Page/Svod2Page.jsx @@ -6,11 +6,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch'; +import { formatHeaderUnit } from '../../components/RealtimeTable/utils/headerYearUtils'; +import { getDisplayFactor } from '../../components/RealtimeTable/utils/moneyUtils'; +import { createFixedRowVirtualizerOptions, getRootFontSizePx } from '../../components/RealtimeTable/utils/virtualizerOptions'; import { PrimaryButton } from '../../components/common/Buttons/Buttons'; import DenominationSelect from '../../components/common/DenominationSelect'; import useDenomination from '../../components/common/useDenomination'; -import { getDisplayFactor } from '../../components/RealtimeTable/utils/moneyUtils'; -import { createFixedRowVirtualizerOptions, getRootFontSizePx } from '../../components/RealtimeTable/utils/virtualizerOptions'; import { useSvod } from '../SvodPage/SvodContext'; import { SvodFilters } from '../SvodPage/SvodFilters'; @@ -99,7 +100,7 @@ const createPaymentColumns = (quarter) => ({ header: 'Платеж и предоставление акта', columns: [ textColumn(`q${quarter}.payment_date`, 'Дата платежа', [`q${quarter}`, 'payment_date'], 9), - numericColumn(`q${quarter}.payment_amount`, 'Сумма, тыс. руб. (без НДС)', [`q${quarter}`, 'payment_amount'], 12), + numericColumn(`q${quarter}.payment_amount`, 'Сумма, {unit} (без НДС)', [`q${quarter}`, 'payment_amount'], 12), numericColumn(`q${quarter}.payment_amount_ho`, 'в т.ч. сумма (без НДС) ГО', [`q${quarter}`, 'payment_amount_ho'], 12), numericColumn(`q${quarter}.payment_amount_rf`, 'в т.ч. сумма (без НДС) РФ', [`q${quarter}`, 'payment_amount_rf'], 12), textColumn(`q${quarter}.payment_act`, 'Предоставление акта (др. подтверждения)', [`q${quarter}`, 'payment_act'], 16), @@ -269,11 +270,11 @@ const createColumns = (sheet) => [ textColumn('contract.reference', 'Договор (реквизиты)', ['contract_detail', 'reference'], 12), textColumn('contract.subject', 'Предмет', ['contract_detail', 'subject'], 18), textColumn('contract.currency', 'Валюта договора', ['contract_detail', 'currency'], 9), - numericColumn('contract.ceiling', 'Предельная ст-ть, тыс. руб. (без НДС)', ['contract_detail', 'ceiling_amount'], 14), + numericColumn('contract.ceiling', 'Предельная ст-ть, {unit} (без НДС)', ['contract_detail', 'ceiling_amount'], 14), ...QUARTERS.map((quarter) => numericColumn( `contract.expenses_q${quarter}`, - `Расходы ${quarter} кв., тыс. руб. (без НДС)`, + `Расходы ${quarter} кв., {unit} (без НДС)`, ['contract_detail', `expenses_q${quarter}`], 13, ), @@ -348,12 +349,14 @@ const getColumnConfig = (sheet) => { return COLUMN_CONFIG_CACHE.get(sheet); }; -const scaleNumericColumns = (columns, factor) => columns.map((column) => { - const children = column.columns ? scaleNumericColumns(column.columns, factor) : undefined; - if (children) return { ...column, columns: children }; - if (!column.meta?.money) return column; - return { ...column, Cell: ({ cell }) => formatValue(cell.getValue(), factor) }; -}); +const scaleNumericColumns = (columns, factor, denomination) => + columns.map((column) => { + const children = column.columns ? scaleNumericColumns(column.columns, factor, denomination) : undefined; + const header = formatHeaderUnit(column.header, denomination); + if (children) return { ...column, header, columns: children }; + if (!column.meta?.money) return { ...column, header }; + return { ...column, header, Cell: ({ cell }) => formatValue(cell.getValue(), factor) }; + }); const matchesSearch = (row, query) => { const searchableData = { @@ -466,7 +469,7 @@ export default function Svod2Page() { return normalizedSearch ? rows.filter((row) => matchesSearch(row, normalizedSearch)) : rows; }, [hasCurrentData, rows, search]); const { columns: rawColumns, columnBands, columnGroupEnds } = useMemo(() => getColumnConfig(sheet), [sheet]); - const columns = useMemo(() => scaleNumericColumns(rawColumns, factor), [rawColumns, factor]); + const columns = useMemo(() => scaleNumericColumns(rawColumns, factor, denomination), [rawColumns, factor, denomination]); const table = useMaterialReactTable({ columns, data: filteredRows, @@ -601,7 +604,12 @@ export default function Svod2Page() { onChange={(_event, value) => setSheet(value)} sx={{ px: '1rem', minHeight: '3rem', borderBottom: '0.0625rem solid #edf0f2' }}> {SHEETS.map((item) => ( - + ))} rem * 16; // Высота строки задана в rem явно (строка и ячейка), виртуализатор layout не читает. @@ -63,30 +64,36 @@ export const countSummaryTree = (tree) => tree.reduce((count, row) => count + 1 const denominationKey = (tableKey) => `summary_${tableKey}_denomination_v2`; -const scaleColumns = (columns, factor) => columns.map((column) => { - const children = column.columns ? scaleColumns(column.columns, factor) : undefined; - const originalCell = column.Cell; - const isMoney = Boolean(column.meta?.money); - return { - ...column, - ...(children ? { columns: children } : {}), - ...(isMoney ? { Cell: ({ cell }) => { - const value = cell.getValue(); - if (value === null || value === undefined || value === '') return originalCell ? originalCell({ cell }) : '—'; - const numeric = Number(value); - if (!Number.isFinite(numeric)) return originalCell ? originalCell({ cell }) : value; - if (numeric === 0) return '-'; - return new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 2 }).format(numeric * factor); - } } : {}), - }; -}); +const scaleColumns = (columns, factor, denomination) => + columns.map((column) => { + const children = column.columns ? scaleColumns(column.columns, factor, denomination) : undefined; + const originalCell = column.Cell; + const isMoney = Boolean(column.meta?.money); + return { + ...column, + header: formatHeaderUnit(column.header, denomination), + ...(children ? { columns: children } : {}), + ...(isMoney + ? { + Cell: ({ cell }) => { + const value = cell.getValue(); + if (value === null || value === undefined || value === '') return originalCell ? originalCell({ cell }) : '—'; + const numeric = Number(value); + if (!Number.isFinite(numeric)) return originalCell ? originalCell({ cell }) : value; + if (numeric === 0) return '-'; + return new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 2 }).format(numeric * factor); + }, + } + : {}), + }; + }); export const SummaryTable = memo(function SummaryTable({ columns, data, loading, colorBySource = false, tableKey = 'default' }) { const storageKey = denominationKey(tableKey); const { denomination } = useDenomination(storageKey); const factor = getDisplayFactor('thousand', denomination); - const scaledColumns = useMemo(() => scaleColumns(columns, factor), [columns, factor]); + const scaledColumns = useMemo(() => scaleColumns(columns, factor, denomination), [columns, factor, denomination]); const table = useMaterialReactTable({ columns: scaledColumns, data, diff --git a/web/src/pages/SvodPage/SvodPage.jsx b/web/src/pages/SvodPage/SvodPage.jsx index ce375f7..3b692c5 100644 --- a/web/src/pages/SvodPage/SvodPage.jsx +++ b/web/src/pages/SvodPage/SvodPage.jsx @@ -16,16 +16,16 @@ const SHEETS = [ { value: 'FORM_4', label: 'Проектная деятельность' }, ]; const METRICS = [ - { key: 'plan', label: 'План' }, - { key: 'approved', label: 'Утверждено' }, - { key: 'fact', label: 'Факт' }, - { key: 'corrected', label: 'Скорректированный план' }, + { key: 'plan', label: 'План, {unit}' }, + { key: 'approved', label: 'Утверждено, {unit}' }, + { key: 'fact', label: 'Факт, {unit}' }, + { key: 'corrected', label: 'Скорректированный план, {unit}' }, ]; const FORM_3_METRIC_LABELS = { - plan: 'Расходы, планируемые РФ', - approved: 'Утвержденные базовые расходы', - fact: 'Фактические расходы', - corrected: 'Скорректированная смета расходов', + plan: 'Расходы, планируемые РФ, {unit}', + approved: 'Утвержденные базовые расходы, {unit}', + fact: 'Фактические расходы, {unit}', + corrected: 'Скорректированная смета расходов, {unit}', }; const DIRECTIONS = [ { key: 'support', label: 'Поддержка' }, -- 2.47.2