From b04c87f47c60b17ce4f41d3bdf9c8514f749ca41 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Fri, 30 Oct 2026 22:04:02 +0400 Subject: [PATCH] =?UTF-8?q?fix(export):=20=D0=B2=D1=8B=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=B7=D0=BA=D0=B0=20=D1=84=D0=BE=D1=80=D0=BC=201=20=D0=B8=204?= =?UTF-8?q?=20=E2=80=94=20=D0=BF=D0=BE=D1=80=D1=8F=D0=B4=D0=BE=D0=BA=20?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D0=BB=D0=B1=D1=86=D0=BE=D0=B2=20=D0=B8=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2=D0=BA=D0=B8=20=D0=BA?= =?UTF-8?q?=D0=B0=D0=BA=20=D0=BD=D0=B0=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Выгрузка Excel листов АХР/КВ/Операц/Структура форм 1 и 4 расходилась с экраном по нескольким независимым причинам: - FORM_4: _patch_form4_regular_columns() вызывался дважды — второй вызов стирал только что добавленные actual_rsbu_spod_q1..q3, поэтому в выгрузке были только «Факт 4 кв + СПОД», без «Факт 1/2/3 кв». Убрал второй вызов и больше не удаляю эти ключи в самом патче. - _patch_form4_actual_rsbu_spod_columns: SQL отдаёт actual_rsbu_spod только для 4 кв.; для 1-3 кв. ключ теперь q{n}.actual_quarter (как на экране), не q{n}.actual_rsbu_spod (всегда пустой). - Базовая раскладка FORM1_DEPTH_COLUMNS (отражается и в FORM_4, т.к. она из неё строится): убрал collegial.empty и группы "shift" (ключ sum, SQL не отдаёт); в q2/q3 добавил comment_correct (adj_comment) сразу после reserve_correct — раньше его не было вовсе; в q4 то же поле вынес из rev_correct в отдельный comment_correct на то же место (было в начале квартала вместе с комментарием корректировки плана). group "approved.plan" переименована из скопированного "Резерв Финансового Комитета" в "План {year}"; q4 "quarter_correct" — из ошибочного "Факт РСБУ 3 кв" в "Факт РСБУ 4 кв {year_short}". - render_column_years: добавлен плейсхолдер year_plus_2 (year+2); contract_summary получил future_y2 "Платежи в {year_plus_2} году" (раньше его не было, и FORM_4 patch всё равно дублировал его с неверным {next_year}). На экране «Операц» формы 1 такой колонки нет — для неё новая FORM1_OPER_DEPTH_COLUMNS без future_y2. - FORM4_OPER: proc_plan/proc_method после переименования уезжали в конец блока ckk (pop+переприсвоение добавляет ключ в конец словаря) — вернул порядок экрана через _reorder_children. - web/.../FORM_1/{AHR,CAP,OPER}.js: в q4 узел "Комментарий" (adj_comment, GG) шёл после "Скорректированный план" (corrected_plan, GH) — переставил перед, как в 1-3 кв. Issue: #253 (AURORA-1905, эпик AURORA-1876, Высокий приоритет) Co-Authored-By: Claude Sonnet 5 --- .../export_service/export_mappings.py | 88 +++++++------- .../services/export_service/export_service.py | 4 +- .../unit/test_export_form1_form4_mapping.py | 107 ++++++++++++++++++ .../RealtimeTable/constants/FORM_1/AHR.js | 24 ++-- .../RealtimeTable/constants/FORM_1/CAP.js | 16 +-- .../RealtimeTable/constants/FORM_1/OPER.js | 16 +-- .../utils/form1Q4CommentOrder.test.js | 35 ++++++ 7 files changed, 214 insertions(+), 76 deletions(-) create mode 100644 api/tests/unit/test_export_form1_form4_mapping.py create mode 100644 web/src/components/RealtimeTable/utils/form1Q4CommentOrder.test.js diff --git a/api/src/services/export_service/export_mappings.py b/api/src/services/export_service/export_mappings.py index 2398a18..ae9a898 100644 --- a/api/src/services/export_service/export_mappings.py +++ b/api/src/services/export_service/export_mappings.py @@ -90,6 +90,9 @@ FORM1_DEPTH_COLUMNS: dict = { "future_y1": { "name": "Платежи в {next_year} году", }, + "future_y2": { + "name": "Платежи в {year_plus_2} году", + }, "other_ssp": { "name": "Сумма в других ССП", }, @@ -170,7 +173,7 @@ FORM1_DEPTH_COLUMNS: dict = { "name": "Итоговая смета расходов", "children": { "plan": { - "name": "Резерв Финансового Комитета", + "name": "План {year}", "key": None, "children": { "q1": { @@ -205,9 +208,6 @@ FORM1_DEPTH_COLUMNS: dict = { "protocol": { "name": "Реквизиты протокола (дата, номер)", }, - "empty": { - "name": "", - }, "note": { "name": "Примечание", }, @@ -352,15 +352,6 @@ FORM1_DEPTH_COLUMNS: dict = { } } }, - "shift": { - "name": "Перенос во 2 кв {year_short}", - "key": None, - "children": { - "sum": { - "name": "Сумма, тыс. руб. (с учетом НДC)", - } - } - }, "comment_correct": { "name": "Комментарий", "key": None, @@ -595,11 +586,11 @@ FORM1_DEPTH_COLUMNS: dict = { } } }, - "shift": { - "name": "Перенос в 3 кв {year_short}", + "comment_correct": { + "name": "Комментарий", "key": None, "children": { - "sum": { + "adj_comment": { "name": "Сумма, тыс. руб. (с учетом НДC)", } } @@ -822,11 +813,11 @@ FORM1_DEPTH_COLUMNS: dict = { } } }, - "shift": { - "name": "Перенос в 4 кв {year_short}", + "comment_correct": { + "name": "Комментарий", "key": None, "children": { - "sum": { + "adj_comment": { "name": "Сумма, тыс. руб. (с учетом НДC)", } } @@ -994,9 +985,6 @@ FORM1_DEPTH_COLUMNS: dict = { "rev_comment": { "name": "Сумма, тыс. руб. (с учетом НДC)", }, - "adj_comment": { - "name": "Сумма, тыс. руб. (с учетом НДC)", - }, } }, "new_plan_correct": { @@ -1049,6 +1037,15 @@ FORM1_DEPTH_COLUMNS: dict = { } } }, + "comment_correct": { + "name": "Комментарий", + "key": None, + "children": { + "adj_comment": { + "name": "Сумма, тыс. руб. (с учетом НДC)", + } + } + }, "plan_correct": { "name": "Скорр. план 4 кв. {year_short}", "key": None, @@ -1122,7 +1119,7 @@ FORM1_DEPTH_COLUMNS: dict = { } }, "quarter_correct": { - "name": "Факт РСБУ 3 кв {year_short}", + "name": "Факт РСБУ 4 кв {year_short}", "key": None, "children": { "actual_quarter": { @@ -3243,6 +3240,12 @@ def _rename_color_paths(colors: dict, renames: dict[str, str]) -> dict: return renamed +# На экране «Операц» формы 1 нет колонки «Платежи в {year+2} году» (она есть +# только на АХР/КВ) — отдельная раскладка без contract_summary.future_y2 (AURORA-1905). +FORM1_OPER_DEPTH_COLUMNS: dict = copy.deepcopy(FORM1_DEPTH_COLUMNS) +FORM1_OPER_DEPTH_COLUMNS["upper_ssp_smeta"]["children"]["contract_summary"]["children"].pop("future_y2", None) + + FORM4_DEPTH_COLUMNS: dict = _rename_depth_columns( FORM1_DEPTH_COLUMNS, FORM4_COLUMN_RENAMES, @@ -3283,26 +3286,8 @@ def _patch_form4_regular_columns() -> None: FORM4_DEPTH_COLUMNS["header"]["children"].pop(key, None) FORM4_STRUCTURE_DEPTH_COLUMNS["header"]["children"].pop("justification", None) - for quarter in ("q2", "q3"): - FORM4_DEPTH_COLUMNS[quarter]["children"].setdefault( - "comment_correct", - { - "key": None, - "name": "Комментарий", - "children": {"adj_comment": {"name": "Сумма"}}, - }, - ) - FORM4_STRUCTURE_DEPTH_COLUMNS[quarter]["children"].setdefault( - "comment_correct", - { - "key": None, - "name": "Комментарий", - "children": {"adj_comment": {"name": "Сумма"}}, - }, - ) - - for quarter in (1, 2, 3): - FORM4_DEPTH_COLUMNS.pop(f"actual_rsbu_spod_q{quarter}", None) + # comment_correct (q2/q3) теперь приходит из FORM1_DEPTH_COLUMNS уже на нужном + # месте — отдельный setdefault здесь больше не нужен (AURORA-1905). FORM4_DEPTH_COLUMNS["totals"]["children"].pop("pay_year", None) @@ -3326,7 +3311,7 @@ def _patch_form4_contract_summary_columns() -> None: summary_parent = FORM4_DEPTH_COLUMNS["upper_ssp_smeta"]["children"]["contract_summary"] summary = summary_parent["children"] summary["reference"] = {"name": "Реквизиты договора"} - summary["future_y2"] = {"name": "Платежи в {next_year} году"} + summary["future_y2"] = {"name": "Платежи в {year_plus_2} году"} summary["centralized_flag"] = {"name": "Централизованный договор"} summary_parent["children"] = _reorder_children( summary, @@ -3338,7 +3323,7 @@ def _patch_form4_contract_summary_columns() -> None: structure_summary_parent = FORM4_STRUCTURE_DEPTH_COLUMNS["upper_ssp_smeta"]["children"]["contract_summary"] structure_summary = structure_summary_parent["children"] - structure_summary["future_payments_y2"] = {"name": "Платежи в {next_year} году"} + structure_summary["future_payments_y2"] = {"name": "Платежи в {year_plus_2} году"} structure_summary_parent["children"] = _reorder_children( structure_summary, ( @@ -3349,14 +3334,17 @@ def _patch_form4_contract_summary_columns() -> None: def _patch_form4_actual_rsbu_spod_columns() -> None: + # SQL отдаёт actual_rsbu_spod только для 4 кв.; 1-3 кв. показывают то же + # значение, что и q{n}.actual_quarter внутри самого квартала (AURORA-1905). totals = FORM4_DEPTH_COLUMNS.pop("totals") for quarter in range(1, 5): + leaf_key = f"q{quarter}.actual_rsbu_spod" if quarter == 4 else f"q{quarter}.actual_quarter" FORM4_DEPTH_COLUMNS[f"actual_rsbu_spod_q{quarter}"] = { "key": None, "name": f"Факт {quarter} кв" if quarter < 4 else "Факт 4 кв + СПОД", "children": { "actual_rsbu_spod": { - "key": f"q{quarter}.actual_rsbu_spod", + "key": leaf_key, "name": "", }, }, @@ -3368,7 +3356,6 @@ _patch_form4_regular_columns() _patch_form4_header_columns() _patch_form4_contract_summary_columns() _patch_form4_actual_rsbu_spod_columns() -_patch_form4_regular_columns() FORM4_OPER_DEPTH_COLUMNS: dict = copy.deepcopy(FORM4_DEPTH_COLUMNS) FORM4_OPER_DEPTH_COLUMNS["empty_upper"]["children"]["ckk"]["children"]["procurement_plan"] = ( @@ -3377,6 +3364,12 @@ FORM4_OPER_DEPTH_COLUMNS["empty_upper"]["children"]["ckk"]["children"]["procurem FORM4_OPER_DEPTH_COLUMNS["empty_upper"]["children"]["ckk"]["children"]["procurement_method"] = ( FORM4_OPER_DEPTH_COLUMNS["empty_upper"]["children"]["ckk"]["children"].pop("proc_method") ) +# Переименование выше (pop+присвоение) уезжает в конец словаря — возвращаем +# порядок экрана: ceiling, q1-q4, rf_schedule, deadline, затем закупки, comment (AURORA-1905). +FORM4_OPER_DEPTH_COLUMNS["empty_upper"]["children"]["ckk"]["children"] = _reorder_children( + FORM4_OPER_DEPTH_COLUMNS["empty_upper"]["children"]["ckk"]["children"], + ("ceiling", "q1", "q2", "q3", "q4", "rf_schedule", "deadline", "procurement_plan", "procurement_method", "comment"), +) FORM4_OPER_COLORS: dict = _rename_color_paths( FORM4_COLORS["OPER"], { @@ -4004,6 +3997,7 @@ def render_column_years(columns: dict, year: int) -> dict: "year_short": f"{year % 100:02d}", "next_year": str(year + 1), "next_year_short": f"{(year + 1) % 100:02d}", + "year_plus_2": str(year + 2), } _replace_column_year_placeholders(rendered, values) diff --git a/api/src/services/export_service/export_service.py b/api/src/services/export_service/export_service.py index 00fe243..98cbf62 100644 --- a/api/src/services/export_service/export_service.py +++ b/api/src/services/export_service/export_service.py @@ -33,6 +33,7 @@ from src.services.export_service.export_normalizers import ( from src.services.export_service.export_mappings import ( FORM1_COLORS, FORM1_DEPTH_COLUMNS, + FORM1_OPER_DEPTH_COLUMNS, FORM2_DEPTH_AHR_SUB_COLUMNS, FORM3_COLORS, FORM4_COLORS, @@ -927,7 +928,8 @@ class ExportService: } else: writer_configs = { - "OPER": (FORM1_DEPTH_COLUMNS, FORM1_COLORS["OPER"]), + # AURORA-1905: на экране «Операц» формы 1 нет «Платежи в {year+2} году». + "OPER": (FORM1_OPER_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), diff --git a/api/tests/unit/test_export_form1_form4_mapping.py b/api/tests/unit/test_export_form1_form4_mapping.py new file mode 100644 index 0000000..ad707ac --- /dev/null +++ b/api/tests/unit/test_export_form1_form4_mapping.py @@ -0,0 +1,107 @@ +import importlib + +from xlsxwriter import Workbook as XlsxWorkbook + +from src.services.export_service import export_mappings +from src.services.export_service.export_mappings import ( + FORM1_DEPTH_COLUMNS, + FORM1_OPER_DEPTH_COLUMNS, + FORM4_DEPTH_COLUMNS, + FORM4_OPER_DEPTH_COLUMNS, + FORM4_STRUCTURE_DEPTH_COLUMNS, + render_column_years, +) +from src.services.export_service.export_writers import XlsxDepthSheetWriter + + +def _flat(columns_dict): + writer = XlsxDepthSheetWriter( + workbook=XlsxWorkbook(), + columns_dict=columns_dict, + row_normalizer=lambda row: row, + value_serializer=lambda value: value, + ) + return writer.columns + + +def _tree_node(tree, path): + segments = path.split(".") + node = tree[segments[0]] + for segment in segments[1:]: + node = node["children"][segment] + return node + + +def test_no_stray_collegial_empty_column(): + for columns in (FORM1_DEPTH_COLUMNS, FORM4_DEPTH_COLUMNS, FORM4_STRUCTURE_DEPTH_COLUMNS): + assert "collegial.empty" not in _flat(columns) + + +def test_no_shift_sum_columns_in_base_form1(): + flat = _flat(FORM1_DEPTH_COLUMNS) + assert not [key for key in flat if key.endswith(".sum")] + + +def test_form4_has_fact_q1_q2_q3_and_fact_4_plus_spod_before_totals(): + flat = _flat(FORM4_DEPTH_COLUMNS) + # 1-3 кв. показывают то же значение, что q{n}.actual_quarter внутри квартала + # (сознательный дубль ключа — влияет только на цвет, см. AURORA-1905); 4 кв. + # использует свой собственный q4.actual_rsbu_spod (SQL отдаёт только его). + assert flat.count("q1.actual_quarter") == 2 + assert flat.count("q2.actual_quarter") == 2 + assert flat.count("q3.actual_quarter") == 2 + assert flat.count("q4.actual_rsbu_spod") == 1 + totals_index = flat.index("totals.fact_year") + assert flat.index("q4.actual_rsbu_spod") < totals_index + + +def test_adj_comment_immediately_follows_adj_reserve(): + for quarter in ("q2", "q3", "q4"): + for columns in (FORM1_DEPTH_COLUMNS, FORM4_DEPTH_COLUMNS, FORM4_STRUCTURE_DEPTH_COLUMNS): + flat = _flat(columns) + reserve_index = flat.index(f"{quarter}.adj_reserve") + comment_index = flat.index(f"{quarter}.adj_comment") + assert comment_index == reserve_index + 1, (columns is FORM1_DEPTH_COLUMNS, quarter) + + +def test_form4_oper_ckk_procurement_before_comment(): + flat = _flat(FORM4_OPER_DEPTH_COLUMNS) + assert flat.index("ckk.procurement_plan") < flat.index("ckk.comment") + assert flat.index("ckk.procurement_method") < flat.index("ckk.comment") + assert flat.index("ckk.deadline") < flat.index("ckk.procurement_plan") + + +def test_form1_oper_has_no_future_y2_but_ahr_cap_do(): + assert "contract_summary.future_y2" in _flat(FORM1_DEPTH_COLUMNS) + assert "contract_summary.future_y2" not in _flat(FORM1_OPER_DEPTH_COLUMNS) + + +def test_render_column_years_form4(): + rendered = render_column_years(FORM4_DEPTH_COLUMNS, 2026) + assert _tree_node(rendered, "upper_ssp_smeta.contract_summary.future_y2")["name"] == "Платежи в 2028 году" + assert _tree_node(rendered, "approved.plan")["name"] == "План 2026" + assert _tree_node(rendered, "q4.quarter_correct")["name"] == "Факт РСБУ 4 кв 26" + + +def test_render_column_years_form4_structure(): + rendered = render_column_years(FORM4_STRUCTURE_DEPTH_COLUMNS, 2026) + assert ( + _tree_node(rendered, "upper_ssp_smeta.contract_summary.future_payments_y2")["name"] + == "Платежи в 2028 году" + ) + + +def test_render_column_years_form1(): + rendered = render_column_years(FORM1_DEPTH_COLUMNS, 2026) + assert _tree_node(rendered, "approved.plan")["name"] == "План 2026" + assert _tree_node(rendered, "q4.quarter_correct")["name"] == "Факт РСБУ 4 кв 26" + + +def test_module_reload_is_idempotent(): + # Патчи FORM_4 применяются один раз при импорте модуля; повторный импорт + # не должен снова звать патчи и удваивать/ломать раскладку. + before = _flat(export_mappings.FORM4_DEPTH_COLUMNS) + importlib.reload(export_mappings) + after = _flat(export_mappings.FORM4_DEPTH_COLUMNS) + assert before == after + importlib.reload(export_mappings) diff --git a/web/src/components/RealtimeTable/constants/FORM_1/AHR.js b/web/src/components/RealtimeTable/constants/FORM_1/AHR.js index 37aeb0f..f5ab8eb 100644 --- a/web/src/components/RealtimeTable/constants/FORM_1/AHR.js +++ b/web/src/components/RealtimeTable/constants/FORM_1/AHR.js @@ -3568,14 +3568,14 @@ export const config = { }, }, { - header: 'Скорректированный план 4 кв. {year:short}', - accessorKey: '4_kvartal_2026_skorr_plan_4_kv_26', + header: 'Комментарий', + accessorKey: '4_kvartal_2024_kommentariy', columns: [ { - header: 'Сумма, тыс. руб. (без НДС)', - accessorKey: 'data.q4.corrected_plan', - columnLetter: 'GH', - size: 250, + header: 'Сумма, тыс. руб. (с учётом НДС)', + accessorKey: 'data.q4.adj_comment', + columnLetter: 'GG', + size: 300, filterFn: 'contains', }, ], @@ -3587,14 +3587,14 @@ export const config = { }, }, { - header: 'Комментарий', - accessorKey: '4_kvartal_2024_kommentariy', + header: 'Скорректированный план 4 кв. {year:short}', + accessorKey: '4_kvartal_2026_skorr_plan_4_kv_26', columns: [ { - header: 'Сумма, тыс. руб. (с учётом НДС)', - accessorKey: 'data.q4.adj_comment', - columnLetter: 'GG', - size: 300, + header: 'Сумма, тыс. руб. (без НДС)', + accessorKey: 'data.q4.corrected_plan', + columnLetter: 'GH', + size: 250, filterFn: 'contains', }, ], diff --git a/web/src/components/RealtimeTable/constants/FORM_1/CAP.js b/web/src/components/RealtimeTable/constants/FORM_1/CAP.js index e8bce1b..6731fa5 100644 --- a/web/src/components/RealtimeTable/constants/FORM_1/CAP.js +++ b/web/src/components/RealtimeTable/constants/FORM_1/CAP.js @@ -3582,13 +3582,13 @@ export const config = { }, }, { - header: 'Скорректированный план 4 кв. {year:short}', - accessorKey: '4_kvartal_2024_skorr_plan_4_kv_24', + header: 'Комментарий', + accessorKey: '4_kvartal_2024_kommentariy', columns: [ { header: 'Сумма, тыс. руб. (с учётом НДС)', - accessorKey: 'data.q4.corrected_plan', - columnLetter: 'GH', + accessorKey: 'data.q4.adj_comment', + columnLetter: 'GG', size: 300, filterFn: 'contains', }, @@ -3601,13 +3601,13 @@ export const config = { }, }, { - header: 'Комментарий', - accessorKey: '4_kvartal_2024_kommentariy', + header: 'Скорректированный план 4 кв. {year:short}', + accessorKey: '4_kvartal_2024_skorr_plan_4_kv_24', columns: [ { header: 'Сумма, тыс. руб. (с учётом НДС)', - accessorKey: 'data.q4.adj_comment', - columnLetter: 'GG', + accessorKey: 'data.q4.corrected_plan', + columnLetter: 'GH', size: 300, filterFn: 'contains', }, diff --git a/web/src/components/RealtimeTable/constants/FORM_1/OPER.js b/web/src/components/RealtimeTable/constants/FORM_1/OPER.js index 2d1378e..4e5f839 100644 --- a/web/src/components/RealtimeTable/constants/FORM_1/OPER.js +++ b/web/src/components/RealtimeTable/constants/FORM_1/OPER.js @@ -3566,13 +3566,13 @@ export const config = { }, }, { - header: 'Скорректированный план 4 кв. {year:short}', - accessorKey: '4_kvartal_2024_skorr_plan_4_kv_24', + header: 'Комментарий', + accessorKey: '4_kvartal_2024_kommentariy', columns: [ { header: 'Сумма, тыс. руб. (с учётом НДС)', - accessorKey: 'data.q4.corrected_plan', - columnLetter: 'GH', + accessorKey: 'data.q4.adj_comment', + columnLetter: 'GG', size: 300, filterFn: 'contains', }, @@ -3585,13 +3585,13 @@ export const config = { }, }, { - header: 'Комментарий', - accessorKey: '4_kvartal_2024_kommentariy', + header: 'Скорректированный план 4 кв. {year:short}', + accessorKey: '4_kvartal_2024_skorr_plan_4_kv_24', columns: [ { header: 'Сумма, тыс. руб. (с учётом НДС)', - accessorKey: 'data.q4.adj_comment', - columnLetter: 'GG', + accessorKey: 'data.q4.corrected_plan', + columnLetter: 'GH', size: 300, filterFn: 'contains', }, diff --git a/web/src/components/RealtimeTable/utils/form1Q4CommentOrder.test.js b/web/src/components/RealtimeTable/utils/form1Q4CommentOrder.test.js new file mode 100644 index 0000000..26fb8e2 --- /dev/null +++ b/web/src/components/RealtimeTable/utils/form1Q4CommentOrder.test.js @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +// AURORA-1905: в 1-3 кв. "Комментарий" (adj_comment) уже шёл сразу после +// "Корректировки из резерва" (adj_reserve), перед "Скорректированный план" +// (corrected_plan); в 4 кв. порядок был нарушен (corrected_plan раньше adj_comment). +const SHEETS = ['AHR', 'CAP', 'OPER']; + +function extractDataAccessorKeys(source) { + // Пропускаем секцию "colors" (плоская карта, свой, не относящийся к + // расположению столбцов порядок) — берём только дерево "columns". + const columnsStart = source.indexOf('\n\t\tcolumns: ['); + const columnsSource = columnsStart === -1 ? source : source.slice(columnsStart); + const matches = columnsSource.matchAll(/accessorKey:\s*'(data\.[^']+)'/g); + return [...matches].map((m) => m[1]); +} + +for (const sheet of SHEETS) { + test(`FORM_1/${sheet}.js: q4 adj_reserve -> adj_comment -> corrected_plan`, () => { + const path = fileURLToPath(new URL(`../constants/FORM_1/${sheet}.js`, import.meta.url)); + const keys = extractDataAccessorKeys(readFileSync(path, 'utf8')); + + const reserveIndex = keys.indexOf('data.q4.adj_reserve'); + const commentIndex = keys.indexOf('data.q4.adj_comment'); + const planIndex = keys.indexOf('data.q4.corrected_plan'); + + assert.notEqual(reserveIndex, -1, 'data.q4.adj_reserve не найден'); + assert.notEqual(commentIndex, -1, 'data.q4.adj_comment не найден'); + assert.notEqual(planIndex, -1, 'data.q4.corrected_plan не найден'); + assert.ok(reserveIndex < commentIndex, 'adj_comment должен идти после adj_reserve'); + assert.ok(commentIndex < planIndex, 'adj_comment должен идти перед corrected_plan'); + }); +}