310 Commits

Author SHA1 Message Date
aep
616f8cfe0a Merge pull request 'AURORA-1936: запрет удаления чужих строк, подтверждение удаления' (#257) from feat/249-aurora-1936-row-delete-confirm into test 2026-11-08 10:01:19 +03:00
Andrey Epifancev
455393cc76 Merge origin/test в feat/264-aurora-1895-formula-localstorage
Конфликты (EditCell.jsx, RealtimeTable.jsx, formulaUtils.test.js):
- EditCell: сохранены правки 1891 (editorInitialInput, Esc/Tab/Enter,
  возврат фокуса) и 1892 (warnIfNegativeNotAllowed); поверх них
  повторена логика 1895: формула из localStorage подставляется только
  если совпадает с текущим значением, иначе stale и удаляется.
- Сохранённая формула не подставляется, если редактор открыт набором
  символа (getEditorInitialInput) и не затирает набранный текст при
  изменении value другим пользователем (разрешение один раз при открытии).
- Tab/Enter помечают следующую ячейку (openNextEditableCell) только после
  фактического закрытия редактора: при невычисляемой формуле он остаётся
  открытым и не оставляет висящую цель перехода.
- formulaUtils.js: единственный isSameCellValue из test (с нормализацией
  разделителей); дубль из ветки удалён.
- formulaUtils.test.js: тесты обеих сторон.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 10:24:55 +04:00
Andrey Epifancev
3fb1a3b162 Merge remote-tracking branch 'origin/test' into feat/249-aurora-1936-row-delete-confirm 2026-11-08 09:56:16 +04:00
aep
3e00f91c69 Merge pull request 'AURORA-1905: выгрузка форм 1 и 4 — порядок столбцов и заголовки как на экране' (#260) from feat/253-aurora-1905-form1-form4-export-layout into test 2026-11-08 08:51:39 +03:00
aep
e52e8b6cd9 Merge pull request 'AURORA-1902: Смета РФ — выгрузка Операц/АХР/КВ своей раскладкой' (#259) from feat/252-aurora-1902-form2-export-layout into test 2026-11-08 08:51:25 +03:00
aep
e757430801 Merge pull request 'AURORA-1901: Смета РФ — итоги групп и IV кв. на свои места' (#258) from feat/251-aurora-1901-smeta-rf-column-order into test 2026-11-08 08:51:02 +03:00
Andrey Epifancev
5ad5cbb3a3 Мерж feat/252 (AURORA-1902) в feat/253: стек выгрузки FORM_2 + FORM_1/4 (AURORA-1905)
Конфликт export_service.py: «Операц» формы 1 берёт FORM1_OPER_DEPTH_COLUMNS (без «Платежи в year+2»),
остальные листы — кортеж (колонки, цвета, extra, нормализатор) из #259.
Тест test_export_form2_mapping сравнивает Операц Ф.1 с FORM1_OPER_DEPTH_COLUMNS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 09:42:00 +04:00
Andrey Epifancev
a5a4087d9e Мерж origin/test в feat/252: раскладка FORM_2 переносит новые колонки и подписи test (AURORA-1902)
- конфликт export_service.py: кортеж writer_configs расширен до (колонки, цвета, extra, нормализатор);
  FORM_2 использует свой нормализатор и extra {unit}
- АХР формы 2: отдельное дерево FORM2_AHR_DEPTH_COLUMNS с колонками «В пределах лимита, год» и
  «Сверх лимита, год» (AURORA-1907), как на экране
- заголовок группы «Планирование годовых показателей, {unit} (без НДС)» как на экране (AURORA-1900)
- тесты test_export_form2_mapping.py обновлены

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 09:40:56 +04:00
Andrey Epifancev
83372f7aef Merge origin/test в feat/250-aurora-1888-stages-role-filter
Конфликт в api/tests/integration/test_form_phases_api_smoke.py: оставлены
оба набора тестов (тесты фильтра по роли и 404 из этого PR и тест
aware-дат из test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 09:38:56 +04:00
Andrey Epifancev
6e8942f66f Merge remote-tracking branch 'origin/test' into feat/249-aurora-1936-row-delete-confirm
# Conflicts:
#	web/src/components/RealtimeTable/RealtimeTable.jsx
2026-11-08 09:37:02 +04:00
Andrey Epifancev
79fa992ab4 Мерж origin/test в feat/253: разрешены конфликты export_mappings/export_service и FORM_1 AHR/CAP/OPER (AURORA-1905)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 09:31:32 +04:00
Andrey Epifancev
64318458e9 Мерж origin/test в feat/251: конфликт SMETA.js (AURORA-1901)
Группа «В тыс. рублей» удалена, как и задумано в AURORA-1901; правка заголовка
из AURORA-1900 к удалённой группе не применяется.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 09:22:29 +04:00
aep
cc2d2627a1 Merge pull request 'AURORA-1840: состояние модалок листа вынесено из RealtimeTable' (#218) from feat/213-aurora-1840-sheet-modals into test 2026-11-08 07:53:18 +03:00
aep
310f099e7f Merge pull request 'AURORA-1944: таблицы форм — шаг прокрутки колесом мыши ×2' (#247) from feat/246-aurora-1944-wheel-scroll-step into test 2026-11-08 07:52:56 +03:00
aep
8bc2c5fe22 Merge pull request 'AURORA-1877: сессия обрывается через 60 минут — нет обновления access-токена' (#256) from feat/248-aurora-1877-token-refresh into test 2026-11-08 07:52:06 +03:00
PotapovaA
ad709fb0f7 правки 2026-11-06 16:06:35 +03:00
Andrey Epifancev
7d90592676 Merge remote-tracking branch 'origin/feat/284-aurora-1891-cell-click-keyboard-edit' into feat/284-aurora-1891-cell-click-keyboard-edit 2026-11-02 22:05:11 +04:00
Andrey Epifancev
ba167c0e48 Merge remote-tracking branch 'origin/test' into feat/284-aurora-1891-cell-click-keyboard-edit
# Conflicts:
#	web/src/components/RealtimeTable/Cell/Cell/Cell.jsx
#	web/src/components/RealtimeTable/RealtimeTable.jsx
#	web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx
#	web/src/components/RealtimeTable/TableHead/TableHead.jsx
#	web/src/components/RealtimeTable/tableColumns.jsx
2026-11-02 22:04:44 +04:00
Andrey Epifancev
9a151a19eb Merge remote-tracking branch 'origin/test' into feat/272-aurora-1887-ahr-hide-satellite-rows
# Conflicts:
#	web/src/components/RealtimeTable/RealtimeTable.jsx
#	web/src/components/RealtimeTable/utils/rowUtils.test.js
2026-11-02 21:50:17 +04:00
Andrey Epifancev
e755272787 Merge remote-tracking branch 'origin/test' into feat/289-aurora-1900-unit-vat-labels
# Conflicts:
#	web/src/components/RealtimeTable/constants/FORM_1/SMETA.js
2026-11-02 21:47:53 +04:00
Andrey Epifancev
67c192a2e0 Merge remote-tracking branch 'origin/test' into feat/285-aurora-1899-denomination-base-thousand
# Conflicts:
#	web/src/components/RealtimeTable/Cell/Cell/Cell.jsx
2026-11-02 21:45:04 +04:00
aep
12aae02504 Merge pull request 'AURORA-1903: лист «Смета» — своя раскладка колонок для каждой формы' (#287) from feat/286-aurora-1903-smeta-export-layout into test 2026-11-02 20:36:59 +03:00
aep
5fe1264064 Merge pull request 'AURORA-1894: красная рамка у ошибки ячейки, тост без Server error:undefined' (#277) from feat/275-aurora-1894-error-border-toast into test 2026-11-02 20:34:38 +03:00
aep
296f868c4c Merge pull request 'AURORA-1907: признак источника строки АХР и раздельные суммы лимита/сверх лимита' (#271) from feat/270-aurora-1907-source-sheet into test 2026-11-02 20:32:54 +03:00
aep
5317c04d3a Merge pull request 'AURORA-1906: колонка «СПОД» в листах аренды, коммуналки и охраны АХР' (#263) from feat/254-aurora-1906-spod-column into test 2026-11-02 20:30:23 +03:00
PotapovaA
d9a4c89c71 в версии либы icons-material на проде иконки называются иначе + добавление прямой версии в package.json (раннее это требование библиотеки mrt) 2026-11-02 17:10:21 +03:00
Andrey Epifancev
24ca4410f4 feat(labels): единые подписи «тыс. руб.» и НДС на экране и в выгрузке (AURORA-1900)
Экран: {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>
2026-11-01 20:16:27 +04:00
Andrey Epifancev
8558670b46 feat(table): редактирование одним кликом и с клавиатуры, без лишних записей в аудит (AURORA-1891)
- processColumns (tableColumns.jsx): enableEditing для каждой листовой колонки
  через isCellEditable (PR #281), а не только для vsp_dropdown — MRT больше не
  открывает редактор (даже disabled) на нередактируемой ячейке, сервер не берёт
  блокировку, коллеги не видят 🔒.
- Одиночный клик по редактируемой, не заблокированной ячейке открывает
  редактор (muiTableBodyCellProps.onClick, RealtimeTable.jsx); клик по
  нередактируемой/занятой ячейке, как раньше, выделяет строку. Удалён мёртвый
  onClick/handleClick из Cell.jsx.
- Клавиатура на выделенной ячейке: Enter/F2 — открыть редактор; Delete/Backspace
  — открыть с пустым значением; печатный символ — открыть и подставить его
  вместо текущего значения (хранится в state RealtimeTable, читается через
  table.options.meta.getEditorInitialInput).
- EditCell.jsx/VspDropdownEditCell.jsx: Esc — отмена без cell_updated; Tab/
  Shift+Tab и Enter — сохранить и перейти к следующей/предыдущей редактируемой
  ячейке через table.options.meta.openNextEditableCell → utils/cellNavigation.js
  (чистые findNextEditableColumnId/findNextEditableRowId по
  getOrderedVisibleColumns и table.getRowModel().flatRows).
- utils/formulaUtils.js: isSameCellValue — handleSave не шлёт cell_updated,
  если значение не изменилось (число/строка, «1 500,0», пусто/null); формула
  всегда считается изменённой.
- Фокус: setTimeout(50) → requestAnimationFrame; после закрытия редактора
  фокус возвращается в <td> ячейки.

Тесты: utils/cellNavigation.test.js, utils/formulaUtils.test.js (новые,
15 тестов). node --test src/components/RealtimeTable/utils/*.test.js:
85 тестов, 84 passed, 1 известный pre-existing fail (columnLayoutUtils.test.js,
нет node_modules в этом worktree, см. PR #281/#283).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-11-01 16:14:02 +04:00
Andrey Epifancev
733d7a9036 fix(denomination): база разрядности — тыс. руб., масштабировать только денежные колонки
Селектор разрядности (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>
2026-11-01 16:08:20 +04:00
Andrey Epifancev
72f48cc559 fix(export): лист "Смета" выгружается по раскладке экрана каждой формы (AURORA-1903)
До этой правки лист "Смета" всех форм выгружался одной плоской
раскладкой SMETA_COLUMNS (41 колонка) — для FORM_2/FORM_4 в выгрузке
были лишние колонки другого направления (поддержка/развитие), которых
нет на экране. Добавлена SMETA_COLUMNS_BY_FORM{"FORM_1","FORM_2","FORM_4"}
с раскладкой колонок строго в порядке экрана каждой формы;
_create_xlsxwriter_sheet_writers теперь выбирает нужный список по
form_type_code. SMETA_COLUMNS остаётся алиасом на FORM_1-раскладку для
обратной совместимости (неиспользуемый openpyxl-словарь, старый тест).

Также TabularSheetWriter.write и XlsxTabularSheetWriter.write теперь
пропускают служебную строку META (как уже делают depth-писатели) —
раньше она давала пустую вторую строку на листе "Смета".

На экране FORM_1/SMETA.js добавлены "Утверждено ИТОГО Год" (Z) и
"Факт ИТОГО Год" (AL) — SQL (v_form_view) всегда их отдавал и
выгрузка Excel их писала, но экран их терял.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-11-01 16:01:25 +04:00
Andrey Epifancev
d7ab4d170c feat(table): плашка контроля ошибок со счётчиком, переходом к ячейке и подсказкой (AURORA-1893)
- validationUtils.collectInvalidErrors теперь собирает ошибки по ЯЧЕЙКАМ
  (id/ruleId/columnId/lineId/rowLabel/message), а не одну запись на колонку;
  новая groupValidationErrors(errors) группирует их обратно по правилу+колонке
  со счётчиком для меню плашки.
- useValidationRules: getCellErrors(columnId, value, row) отдаёт причины
  конкретной ячейки (для подсказки), isCellInvalid выражен через него;
  сравнение стабильности errors — по id (он уже кодирует правило+колонку+строку).
- RealtimeTable: handleNavigateToError({lineId, columnId}) — находит строку по
  getRowId в table.getRowModel().flatRows (не .rows, т.к. flatRows не фильтруется
  по свёрнутым группам), раскрывает родителей через setExpanded, включает
  скрытую колонку через handleSetColumnsVisibility, выделяет ячейку как
  activeSearchMatch и скроллит к ней — по образцу navigateToSearchMatch.
- ValidationErrorsAlert: красная плашка (#FDEDED/#D32F2F) со счётчиком
  «Ошибок: N», кнопками ‹›  для прохода по всем ошибкам и меню, сгруппированным
  по правилу+колонке (до 20 строк, дальше «и ещё N»); перенесена в начало
  SettingPanel, перед группами настроек.
- tableColumns/Cell: invalidMessage (из getCellErrors) доходит до Cell и
  показывается подсказкой (MUI Tooltip) при наведении на невалидную ячейку.

Тесты (node --test .../validationUtils.test.js): 12/12, включая 5 существующих
тестов nonNegativeRule из AURORA-1892 (не менялись, подтверждают отсутствие
регрессии) и 7 новых для collectInvalidErrors/groupValidationErrors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-11-01 15:11:26 +04:00
Andrey Epifancev
df6ec795a3 feat(table): визуально отличать редактируемые ячейки от расчётных и закрытых (AURORA-1890)
- utils/cellEditability.js: isCellEditable/isComputedColumn — чистые функции,
  isComputedColumn повторяет часть v3._apply_form_cell/_apply_form3_cell
  (0023_apply_cell_empty_string.sql), чтобы админ не видел вычисляемые
  колонки как редактируемые. tableColumns.jsx использует isCellEditable
  вместо инлайновой проверки.
- useRealtimeData.js: возвращает stageColumnDeadlines (closes_at по колонке
  текущего этапа) рядом с columnsCurStage.
- Cell.jsx: редактируемая ячейка — белый фон, рамка 1px #7FA7C9 (2px при
  наведении), курсор text; нередактируемая ячейка строки INPUT — фон
  #F4F5F7, текст #5F6B7A; занятая коллегой — пунктирная оранжевая рамка
  2px #F0A030 без изменения фона. Строки ITEM/GROUP/ROOT не затронуты.
  Сохранён порядок стилей из AURORA-1894 (INVALID_STYLES последним) и
  спред fontWeight для отрицательных чисел из AURORA-1892.
- TableHead.jsx: метка срока ввода «✎ до DD.MM HH:mm» в заголовке колонки
  текущего этапа, только для исполнителя (utils/deadlineFormat.js).
- SettingPanel/CellLegend.jsx: иконка "?" с легендой (можно вводить /
  расчёт или закрыто) рядом с плашкой контроля ошибок, без новой панели
  и без новых цветов в palettes columnColors.js.

node --test: cellEditability.test.js, deadlineFormat.test.js — 18/18.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-11-01 13:00:40 +04:00
Andrey Epifancev
d518961365 Merge remote-tracking branch 'origin/feat/275-aurora-1894-error-border-toast' into feat/276-aurora-1892-negative-amounts
# Conflicts:
#	web/src/components/RealtimeTable/Cell/Cell/Cell.jsx
2026-11-01 12:42:29 +04:00
Andrey Epifancev
fa586c85cc feat(table): отрицательные суммы красным и правило «минус запрещён» (AURORA-1892)
Cell.jsx: formatNumber вынесен в utils/numberFormat.js и возвращает
{ text, isNegative }; отрицательные числа выделяются цветом (#C62828 на
светлом фоне, #FFCDD2 на тёмном) и font-weight 600, ноль остаётся «-».

constants/validation.js: фабрика nonNegativeRule и общий список
nonNegativeColumnKeys (план, бронь на 2026/2027, факт по месяцам и СПОД,
сумма платежа, предельная стоимость договора/ЦКК, сателлиты АХР
Аренда/Коммунальные/Безопасность, АХР Лимиты целиком). Корректировки и
переносы (adj_*, base_correction, revision_*, seq_*, target_change,
transfer_*) не включены — там минус — нормальная операция. Запрет
мягкий: подсветка и запись в «Контроль ошибок» через существующий
useValidationRules/isCellInvalid, сохранение не блокируется.

EditCell.jsx: handleSave предупреждает toast.warning при сохранении
отрицательного числа в защищённой колонке, значение всё равно
сохраняется. useValidationRules и RealtimeTable прокидывают
человекочитаемый заголовок колонки для текста предупреждения.

Тесты (node --test): numberFormat.test.js, validationUtils.test.js —
новые, 10/10 зелёных.
2026-11-01 12:20:06 +04:00
Andrey Epifancev
7514ac9803 fix(realtime): getDataRowId возвращает null вместо undefined, тесты M-8/M-9
После jsonb_strip_nulls у групп/позиций нет ключа line_id, getDataRowId
возвращал undefined, и row_id в сообщении удаления сериализовался в {}
(KeyError на сервере). Возвращаем null, как до фазы B.
Добавлены тесты updateCells (глубокое отсутствие блока, два обновления в батче)
и сверка jsonb_strip_nulls с _strip_nulls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:14:49 +04:00
Andrey Epifancev
c7c679851d fix(realtime): пустая правка уходит как value: null, а не без ключа (AURORA-1848)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:14:49 +04:00
Andrey Epifancev
be8d41ccb5 test(realtime): правка соавтора по ключу, которого нет в строке (AURORA-1848)
Четыре теста updateCells: вставка отсутствующего блока и поля,
явная запись null, вставка отсутствующего скалярного ключа data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:14:49 +04:00
Andrey Epifancev
83cb364a9e fix(realtime): красная рамка у ошибки ячейки, тост без Server error:undefined (AURORA-1894)
Cell.jsx: порядок спреда стилей — base → editable/not-editable → locked →
invalid (INVALID_STYLES последним), чтобы красная рамка 2px не перетиралась
EDITABLE_STYLES у вводимых ячеек с ошибкой.

utils/wsErrorUtils.js: getWsErrorMessage(error) — строка возвращается как
есть, объект берёт message || detail, известные технические префиксы
(computed_field:, role_not_allowed:, window_closed:, org_not_assigned)
заменяются понятным текстом, прочее отдаётся без "Server error:".

RealtimeContext.js: toast.error(getWsErrorMessage(data.error)) вместо
`Server error:${data.error.message}` || '', которое всегда давало undefined
для ошибок-строк (|| не срабатывает на непустой строке).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-11-01 12:10:16 +04:00
aep
26ae77878b Merge pull request 'AURORA-1837: статичные стили ячеек в CSS, overscan' (#230) from feat/226-aurora-1837-cell-styles-css into test 2026-11-01 11:05:50 +03:00
aep
cda8f12492 Merge pull request 'AURORA-1838: updateCells без клонирования всего дерева' (#223) from feat/212-aurora-1838-ws-immutable-update into test 2026-11-01 09:38:39 +03:00
aep
1ece2f3c08 Merge pull request 'AURORA-1836: фиксированная высота строк вместо measureElement' (#222) from feat/211-aurora-1836-row-measure into test 2026-11-01 09:37:38 +03:00
Andrey Epifancev
234103606b feat(form2): скрыть сателлитные строки на листе АХР и заблокировать добавление для аренды/коммуналки/охраны (AURORA-1887)
Упрощённый дизайн (согласован в чате 2026-10-31, расходится с архитектурным
предложением из тикета): без слияния budget_line, без миграции данных, без
изменения протокола WS, без изменения v_svod2_rf.

- v3.v_form2_sheet_jsonb (0054): при p_sheet='AHR' не возвращает INPUT-строки
  с budget_line.source_sheet IN ('AHR_LIMIT','AHR_RENT','AHR_UTILITY',
  'AHR_SECURITY'); ручные строки (включая «сверх лимита», AURORA-1907)
  остаются видимыми. Листы-источники не затронуты. Суммы на заголовках статей
  считаются независимо от видимости строк — не меняются.
- header.can_add_row (новое поле, только p_sheet='AHR'): false для статей
  аренды/коммуналки/охраны (v3._form2_satellite_expense_item, новый helper),
  true для лимитных и обычных статей. Фронт (RealtimeTable.jsx handleAddRow,
  utils/rowUtils.js canAddRowForHeader) блокирует «Добавить строку» по этому
  флагу — тот же паттерн, что у FormsNoCanAddRow/isVspAdditionRow (без
  серверной валидации в add_budget_line).
- Обновлены тесты, которые проверяли суммы/источник через теперь скрытые
  INPUT-строки на AHR (test_form2_ahr_limit.py,
  test_form2_ahr_satellite_spod_integration.py,
  test_form2_ahr_satellite_integration.py) — сравнение переведено на
  заголовок статьи (до/после патча) и на лист-источник.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-31 21:59:04 +04:00
Andrey Epifancev
fe9dfbc7e1 feat(form2): признак источника строки АХР и раздельные суммы лимита/сверх лимита (AURORA-1907)
Миграция 0053: v3.budget_line.source_sheet (NULL | AHR_LIMIT | AHR_RENT |
AHR_UTILITY | AHR_SECURITY) — явный признак источника строки вместо
сопоставления по совпадению имени с LEAF limit_template. Backfill
существующих строк: сателлиты — из v3.form2_ahr_satellite_line_source,
лимитные — минимальный id в группе (форма, статья, имя LEAF), остальные
совпавшие по имени становятся ручными (RAISE NOTICE со списком).

_ensure_form2_ahr_limit_sync_line теперь ищет строку по source_sheet='AHR_LIMIT'
(а не по имени) — тот же класс бага, что чинил #265/AURORA-1909 для сателлитов
аренды/коммуналки/охраны, теперь для лимитов. is_form2_ahr_satellite_line,
del_budget_line (guard на удаление) и get_form2_ahr_satellite_delta переведены
на признак источника. Новая v3._form2_expense_item_has_limit выносит правило
«у статьи есть лимит» в отдельную функцию.

v_form2_sheet_jsonb: header.source ('AHR_LIMIT'/'AHR_RENT'/'AHR_UTILITY'/
'AHR_SECURITY'/'MANUAL'), header.is_over_limit для ручных строк на статьях
с лимитом; plan.limit_year/over_limit_year (только на листе AHR, свёрнуты по
дереву статей через mv_expense_item_tree.desc_ids) — plan.year не меняется,
остаётся суммой всех строк.

Фронт: AHR.js — колонки «В пределах лимита, год»/«Сверх лимита, год» после
«План, год» (нередактируемые — нет случая в v3._apply_form_cell); Cell.jsx —
чип «сверх лимита» в ячейке наименования по header.is_over_limit, без заливки
строки. export_mappings.py — те же два поля в экспорт АХР.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-31 17:54:55 +04:00
Andrey Epifancev
2e6c94fb07 fix(table): формула из localStorage не перезаписывает чужое значение ячейки
Пока AURORA-1896 не перенесёт формулы на сервер, временно исправляем
потерю данных в EditCell:

- resolveEditorState/isSameCellValue (formulaUtils.js) — чистые функции:
  формула подставляется в редактор только если её результат совпадает
  с текущим значением ячейки, иначе она устарела (stale) и удаляется
  из localStorage.
- EditCell: открытие и закрытие ячейки без правки больше не шлёт
  cell_updated (сверка с initialEditorValue при выходе из редактора;
  для обычных значений — дополнительно через isSameCellValue). Если
  введённая формула не вычисляется — toast и редактор остаётся открытым,
  на сервер ничего не уходит.
- tableColumns.jsx: ключ формулы строится от getRowId(cell.row) (line_id
  из данных), а не от cell.row.id (позиционный индекс-путь в дереве,
  переезжающий при вставке/удалении строк). Префикс ключа — formula_v2_.
- RealtimeTable: при монтировании once-очистка устаревших ключей
  formula_* без суффикса _v2 — они привязаны к позиции строки и стали
  недостоверны.

Тесты: web/src/components/RealtimeTable/utils/formulaUtils.test.js,
node --test — 9/9 новых + 5/5 ранее существующих в utils/ зелёные.

Closes #264.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-31 14:11:34 +04:00
Andrey Epifancev
d41668dc27 feat(form2): колонка «СПОД» в сателлитах АХР (аренда/коммуналка/охрана)
Добавлен столбец «СПОД» между «Декабрь» и «Итог IV кв.» на листах
AHR_RENT/AHR_UTILITY/AHR_SECURITY формы 2, с переносом значения в
budget_line_quarter.actual_spod синхронизируемой строки АХР (Support)
и, через уже существующую логику, в итог 4 кв. и года АХР/Смет/сводов.

- rent_detail/utility_detail/security_detail: колонка actual_spod
  (ALTER TABLE + ORM).
- v3.v_form2_view: fact_q4.spod и fact_q4.total с учётом spod в ветках
  AHR_RENT/AHR_UTILITY/AHR_SECURITY.
- v3._apply_form_cell: fact_q4.spod разрешён как обычное поле месяца.
- v3._upsert_form2_ahr_detail_sync_values: DROP+CREATE с новым
  параметром p_actual_spod (DROP обязателен из-за неоднозначности
  перегрузки с DEFAULT).
- v3.sync_form2_ahr_satellite_lines/line: actual_spod передаётся из
  rd/ud/sd в апсерт.
- v3.v_svod2_go: строка аренды также считается заполненной по
  actual_spod.
- Data-миграция: 'fact_q4.spod' добавлен в column_keys этапов, где уже
  есть 'fact_q4.dec' (phase_template/form_phase), идемпотентно.
- Тело CREATE OR REPLACE для v_form2_view/sync_form2_ahr_satellite_*
  собрано вручную по миграциям 0033/0034 + regexp-патчам из 0037
  (pg_get_functiondef на живой БД недоступен в этой среде — см.
  Rulings в PR).

Тесты: node --test на новых accessorKey в конфигах фронта + unit-тест
на 0049/0050 (v_svod2_go теперь засчитывает actual_spod для аренды).
Интеграционные тесты добавлены, но не выполнены — нет доступа к
Postgres в этой рабочей среде (см. Rulings в PR).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-31 00:24:56 +04:00
Andrey Epifancev
c1c0d73387 fix(stages): исполнитель видит только свои этапы, 404 вместо 500 на чужую форму/проект
- FormPhaseRepository.get_list и Form3PhaseRepository.get_list: новый
  необязательный параметр role для фильтрации по v3.role.code.
- FormPhaseService.get_list и Form3PhaseService.get_list: для не-админа
  передают в репозиторий role по коду роли пользователя (единая функция
  role_code_for_user в form_phase_service.py), для ADMIN фильтр не нужен.
- GET /stages/form/{form_id} и GET /stages/project/{project_id}: сначала
  проверяют доступ к форме/проекту (BudgetFormService.get /
  ProjectService.get_instance), при отсутствии доступа — 404 вместо
  AttributeError → 500.
- StagesTable.jsx: подпись роли этапа показывается только админу.

Closes #250.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-31 00:04:21 +04:00
Andrey Epifancev
b04c87f47c fix(export): выгрузка форм 1 и 4 — порядок столбцов и заголовки как на экране
Выгрузка 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 <noreply@anthropic.com>
2026-10-30 22:04:02 +04:00
Andrey Epifancev
daed54d436 fix(export): Смета РФ — выгрузка Операц/АХР/КВ своей раскладкой, не формы 1
Листы Операц/АХР/КВ формы «Смета РФ» (FORM_2) выгружались раскладкой формы 1
(FORM1_DEPTH_COLUMNS, 180 колонок формы 1) — из 163 колонок экрана совпадали
по ключу только 83, 97 колонок выгрузки были всегда пустыми (блоки формы 1,
которых нет в данных формы 2), заголовки групп — от формы 1. Также на экране
был лишний столбец «Адрес ВСП», которого нет в исходной форме РФ, и ВСП
выгружался бы числовым id вместо номера, который виден на экране.

- FORM2_DEPTH_COLUMNS/FORM2_COLORS (export_mappings.py) — сгенерированы из
  web/src/.../FORM_2/{OPER,AHR,CAP}.js (колонки идентичны у всех трёх листов,
  проверено программно; различаются только цвета). 162 листа дерева —
  байт-в-байт тот же порядок, что на экране, минус header.vsp_address.
  Каждая группа — чисто визуальная обёртка ("key": None), каждый лист несёт
  полный путь данных как "key" (например "booking.y2027.q1"), поэтому порядок
  вложенности групп для Excel-заголовков не завязан на реальный путь к данным.
- Form2DepthRowNormalizer (export_normalizers.py) — в отличие от
  Form1DepthRowNormalizer, не поднимает header.* на верхний уровень: FORM_2
  ожидает их как header.section_code, header.vsp_id и т.д.
- _create_xlsxwriter_sheet_writers: отдельная ветка для FormTypeEnum.FORM_2
  (раньше FORM_2 попадала в общий else вместе с FORM_1). Сателлиты АХР
  (AHR_RENT/UTILITY/SECURITY, AURORA-1904) и STRUCTURE не трогал.
- VSPRepository.get_reg_numbers_by_ids + ExportService._resolve_form2_vsp_numbers:
  один запрос резолвит все встретившиеся header.vsp_id (включая закрытые ВСП)
  в registration_number перед записью в Excel, как уже показывает экран.
- web/.../FORM_2/{OPER,AHR,CAP}.js: убран столбец «Адрес ВСП» (E1) и его цвет —
  по умолчанию, т.к. его нет в исходной форме РФ (см. Rulings в PR). Убрана
  мёртвая ссылка на него в Stages/constants.js.

Issue: #252 (AURORA-1902, эпик AURORA-1876, Высокий приоритет)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-30 21:47:24 +04:00
Andrey Epifancev
c01356736f fix(form2): Смета РФ — итоги групп и IV кв. на свои места
На листе «Смета» формы «Смета РФ» итоговые колонки каждой группы
(план/утверждённые/факт «Итого: Поддержка» и IV квартал скорректированной
сметы) были оторваны от своих групп и собраны в отдельную группу
«В тыс. рублей» справа — хотя в шаблоне идут сразу после своего квартала.

- Перенёс H/N/T (data.*.support.year) в конец соответствующих групп план/
  утверждённые/факт, X (data.corrected.support.q4) — в конец группы
  скорректированной сметы после III квартала.
- Удалил опустевшую группу «В тыс. рублей»; accessorKey групп
  (v_tys_rubley_*) нигде больше не использовались (grep по web/src).
  columnLetter и leaf accessorKey (data.*) не менял — видимые значения
  те же, меняется только расположение колонок.
- Тест читает FORM_2/SMETA.js как текст (модуль не грузится напрямую —
  импортирует '../columnColors' без расширения) и проверяет порядок
  accessorKey листьев и отсутствие заголовка «В тыс. рублей».

Issue: #251 (AURORA-1901, эпик AURORA-1876, Высокий приоритет)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-30 18:12:29 +04:00
Andrey Epifancev
b95029f70a fix(forms): запретить удаление чужих строк, добавить подтверждение
Удаление строки в формах 1/2/4 не проверяло владельца: любой исполнитель
мог удалить чужую строку мгновенно, без подтверждения.

- v3.del_budget_line (миграция 0050): не-admin может удалить только
  budget_line, где created_by совпадает с current_user_id() (проверка
  и для обычных листов, и для AHR_RENT/UTILITY/SECURITY — через
  created_by связанной budget_line). Нарушение -> role_not_allowed:
  (403), по существующей конвенции PG_403_ERRORS.
- RealtimeTable: кнопка «Удалить строку» теперь открывает
  ConfirmDeleteRowModal, реальное удаление — только после подтверждения.
- Интеграционные тесты: новый исполнитель isp2 (fixture.sql) в том же
  org_unit, что и budget_form id=1 — проверяют 403 при чужой строке,
  200 при своей и при удалении администратором.

Issue: #249 (AURORA-1936, эпик AURORA-1876, Высокий приоритет)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-30 18:06:52 +04:00