AURORA-1887: скрыть сателлитные строки на листе АХР, заблокировать добавление для аренды/коммуналки/охраны #273

Open
aep wants to merge 6 commits from feat/272-aurora-1887-ahr-hide-satellite-rows into test
Owner

Closes #272.

Контекст

Упрощённый дизайн (согласован в чате с человеком 2026-10-31) — расходится с исходным архитектурным текстом тикета AURORA-1887 (который предлагал слить budget_line по статье + лист-источник, снять UNIQUE(target_line_id) в form2_ahr_satellite_line_source, миграцию данных и правку протокола WS). Согласованная версия ограничивается визуальным скрытием и UI-гейтом, без изменения модели данных:

  • НЕ меняем модель budget_line, НЕ трогаем уникальность в v3.form2_ahr_satellite_line_source.
  • НЕ делаем миграцию слияния существующих строк.
  • НЕ трогаем протокол WS (websocket.py) и v3.v_svod2_rf (Свод 2 — построчная детализация договоров там сознательно остаётся).

Что сделано

  1. Скрытие сателлитных INPUT-строк на листе AHR. v3.v_form2_sheet_jsonb (новая миграция 0054_form2_ahr_hide_satellite_rows.sql, CREATE OR REPLACE полным актуальным текстом + 3 точечных изменения) теперь при p_sheet = 'AHR' не возвращает INPUT-строки, у которых budget_line.source_sheet IN ('AHR_LIMIT','AHR_RENT','AHR_UTILITY','AHR_SECURITY'). Ручные строки (source_sheet IS NULL, включая «сверх лимита» из AURORA-1907) остаются видимыми без изменений. Листы-источники (AHR_LIMIT/AHR_RENT/AHR_UTILITY/AHR_SECURITY) не затронуты — полная детализация там сохраняется.

    Суммы на заголовках статей (plan.year/plan.limit_year/plan.over_limit_year) считаются в v3.v_form2_sheet_sections (CTE tw_agg) и в limit_tree CTE независимо от видимости строк в финальной выдаче — фильтр применяется только к финальному SELECT, логика агрегации не тронута.

  2. Блокировка «Добавить строку» для аренды/коммуналки/охраны. Новое поле header.can_add_row (только при p_sheet = 'AHR') — false для статей аренды/коммуналки/охраны, true для всех прочих (включая лимитные — AURORA-1907 «сверх лимита» не трогаем). Новый небольшой helper v3._form2_satellite_expense_item(p_sheet) возвращает единственный expense_item_id для 'AHR_RENT'/'AHR_UTILITY'/'AHR_SECURITY' (314/24/779 соответственно — для 'AHR_LIMIT' единого id нет, там множество LEAF-статей).

    Фронт (RealtimeTable.jsx handleAddRow, через чистую функцию canAddRowForHeader в utils/rowUtils.js) читает флаг и блокирует клик с тостом «Добавление строк недоступно для этой статьи» — до вызова API.

Рулинги

  • R-1 (механизм блокировки добавления строк). Выбран backend-флаг header.can_add_row, читаемый фронтом, без серверной валидации в v3.add_budget_line. Обоснование: это консистентно с существующими паттернами в этом же компоненте — FormsNoCanAddRow, isVspAdditionRow, isAdditionExpenseItem — ни один из них тоже не продублирован проверкой на backend. v3.add_budget_line и так принимает произвольный expense_item_id на любом сочетании sheet/статья без серверной валидации специфичных для UI правил (убедился чтением кода — тот же паттерн уже используется для AHR_LIMIT в FormsNoCanAddRow, которая тоже чисто фронтовая).
  • R-2 (проверка id статей-сателлитов). 314 (Аренда_ОН) / 24 (Коммунальные услуги) / 779 (Пультовая охрана объектов недвижимости) — проверены не по тексту старых миграций (0034), а напрямую через psql на разовой тестовой Postgres 16 (SELECT id, item_id, name, sheet, direction, depth FROM v3.expense_item WHERE id IN (314,24,779)): все три — sheet='AHR', direction='Support'. Совпадают с актуальным кодом v3.add_budget_line (0051) и v3.sync_form2_ahr_satellite_lines (0052), где эти же id уже захардкожены как текущие (не как "старые" из 0034) — т.е. это не было новым открытием, но было дважды подтверждено независимо.
  • R-3 (WS-протокол не трогается, но проверен). Прочитан код notify_ahr_about_satellite_change/get_form2_ahr_satellite_delta (websocket.py, 0053) и cellUtils.js/RealtimeContext.js на фронте. get_form2_ahr_satellite_delta сама вызывает v3.v_form_view(..., 'AHR', ...), то есть автоматически наследует новый фильтр — скрытая INPUT-строка просто не попадёт в WS-дельту, а websocket.py при отсутствии INPUT-строки в дельте для row_added уже (без изменений с моей стороны) откатывается на cell_updated-событие с заголовками. На фронте cellUtils.deleteRow/updateCells — no-op при несуществующем line_id/row_type+depth+sort_order (прочитано, не изменено). Протокол WS НЕ менялся.
  • R-4 (адаптация тестов, сломанных скрытием строк). Три файла (test_form2_ahr_limit.py, test_form2_ahr_satellite_spod_integration.py, часть test_form2_ahr_satellite_integration.py) проверяли суммы/источник напрямую на теперь скрытых INPUT-строках листа AHR. Переведены на сравнение через заголовок статьи (ITEM/SUB_ITEM/SUB_SUB_ITEM — глубина статьи в дереве расходов разная для разных категорий, проверено эмпирически) по схеме «до/после патча» (устойчиво к существующим сидовым данным), либо через лист-источник, где детализация не пряталась.

Тесты

Интеграционные (api/tests/integration/test_form2_ahr_satellite_integration.py, общий файл серии задач 1907/1909/1887) — 10 новых тестов на все пункты приёмки (скрытие лимитных LEAF с верной суммой на заголовке, скрытие записи аренды/коммуналки/охраны с видимостью на листе-источнике, регрессия на ручную «сверх лимита» строку, can_add_row=false/true, регрессия на обычную статью). Плюс адаптация 3 ранее существовавших тестов, сломанных скрытием строк (см. R-4).

Фронт: canAddRowForHeader (чистая функция в utils/rowUtils.js) покрыта node --test (rowUtils.test.js).

Одноразовая Postgres 16 в Docker (не perf-pg, снесена после работы). cd api && pytest: 247 passed, 3 failed (прежний известный набор, не связанный с этой веткой: test_backend_calls_form3_add_project_and_add_line_functions, test_export_full_form_ignores_non_applicable_params_for_other_sheets, test_export_form_payload_forwards_ignore_flag), 4 skipped — итого 254 (было 244 до +10 новых). cd web && node --test src/components/RealtimeTable/utils/*.test.js: 15 passed.

Test plan

  • TDD: падающий тест → реализация → зелёный прогон
  • Полный api/pytest: разница — ровно +10 новых тестов, прежние 3 известных падения без изменений
  • Фронтовые node --test: 15/15 зелёных
  • perf-pg не затронут (использовалась отдельная одноразовая Postgres 16, снесена)

🤖 Generated with Claude Code

Closes #272. ## Контекст Упрощённый дизайн (согласован в чате с человеком 2026-10-31) — расходится с исходным архитектурным текстом тикета AURORA-1887 (который предлагал слить budget_line по статье + лист-источник, снять UNIQUE(target_line_id) в form2_ahr_satellite_line_source, миграцию данных и правку протокола WS). Согласованная версия ограничивается визуальным скрытием и UI-гейтом, без изменения модели данных: - НЕ меняем модель budget_line, НЕ трогаем уникальность в v3.form2_ahr_satellite_line_source. - НЕ делаем миграцию слияния существующих строк. - НЕ трогаем протокол WS (websocket.py) и v3.v_svod2_rf (Свод 2 — построчная детализация договоров там сознательно остаётся). ## Что сделано 1. **Скрытие сателлитных INPUT-строк на листе AHR.** `v3.v_form2_sheet_jsonb` (новая миграция `0054_form2_ahr_hide_satellite_rows.sql`, CREATE OR REPLACE полным актуальным текстом + 3 точечных изменения) теперь при `p_sheet = 'AHR'` не возвращает `INPUT`-строки, у которых `budget_line.source_sheet IN ('AHR_LIMIT','AHR_RENT','AHR_UTILITY','AHR_SECURITY')`. Ручные строки (`source_sheet IS NULL`, включая «сверх лимита» из AURORA-1907) остаются видимыми без изменений. Листы-источники (`AHR_LIMIT`/`AHR_RENT`/`AHR_UTILITY`/`AHR_SECURITY`) не затронуты — полная детализация там сохраняется. Суммы на заголовках статей (`plan.year`/`plan.limit_year`/`plan.over_limit_year`) считаются в `v3.v_form2_sheet_sections` (CTE `tw_agg`) и в `limit_tree` CTE независимо от видимости строк в финальной выдаче — фильтр применяется только к финальному `SELECT`, логика агрегации не тронута. 2. **Блокировка «Добавить строку» для аренды/коммуналки/охраны.** Новое поле `header.can_add_row` (только при `p_sheet = 'AHR'`) — `false` для статей аренды/коммуналки/охраны, `true` для всех прочих (включая лимитные — AURORA-1907 «сверх лимита» не трогаем). Новый небольшой helper `v3._form2_satellite_expense_item(p_sheet)` возвращает единственный `expense_item_id` для `'AHR_RENT'`/`'AHR_UTILITY'`/`'AHR_SECURITY'` (314/24/779 соответственно — для `'AHR_LIMIT'` единого id нет, там множество LEAF-статей). Фронт (`RealtimeTable.jsx` `handleAddRow`, через чистую функцию `canAddRowForHeader` в `utils/rowUtils.js`) читает флаг и блокирует клик с тостом «Добавление строк недоступно для этой статьи» — до вызова API. ## Рулинги - **R-1 (механизм блокировки добавления строк).** Выбран backend-флаг `header.can_add_row`, читаемый фронтом, **без серверной валидации** в `v3.add_budget_line`. Обоснование: это консистентно с существующими паттернами в этом же компоненте — `FormsNoCanAddRow`, `isVspAdditionRow`, `isAdditionExpenseItem` — ни один из них тоже не продублирован проверкой на backend. `v3.add_budget_line` и так принимает произвольный `expense_item_id` на любом сочетании sheet/статья без серверной валидации специфичных для UI правил (убедился чтением кода — тот же паттерн уже используется для `AHR_LIMIT` в `FormsNoCanAddRow`, которая тоже чисто фронтовая). - **R-2 (проверка id статей-сателлитов).** 314 (Аренда_ОН) / 24 (Коммунальные услуги) / 779 (Пультовая охрана объектов недвижимости) — проверены не по тексту старых миграций (0034), а напрямую через `psql` на разовой тестовой Postgres 16 (`SELECT id, item_id, name, sheet, direction, depth FROM v3.expense_item WHERE id IN (314,24,779)`): все три — `sheet='AHR'`, `direction='Support'`. Совпадают с актуальным кодом `v3.add_budget_line` (0051) и `v3.sync_form2_ahr_satellite_lines` (0052), где эти же id уже захардкожены как текущие (не как "старые" из 0034) — т.е. это не было новым открытием, но было дважды подтверждено независимо. - **R-3 (WS-протокол не трогается, но проверен).** Прочитан код `notify_ahr_about_satellite_change`/`get_form2_ahr_satellite_delta` (websocket.py, 0053) и `cellUtils.js`/`RealtimeContext.js` на фронте. `get_form2_ahr_satellite_delta` сама вызывает `v3.v_form_view(..., 'AHR', ...)`, то есть автоматически наследует новый фильтр — скрытая INPUT-строка просто не попадёт в WS-дельту, а `websocket.py` при отсутствии INPUT-строки в дельте для `row_added` уже (без изменений с моей стороны) откатывается на `cell_updated`-событие с заголовками. На фронте `cellUtils.deleteRow`/`updateCells` — no-op при несуществующем `line_id`/`row_type+depth+sort_order` (прочитано, не изменено). Протокол WS НЕ менялся. - **R-4 (адаптация тестов, сломанных скрытием строк).** Три файла (`test_form2_ahr_limit.py`, `test_form2_ahr_satellite_spod_integration.py`, часть `test_form2_ahr_satellite_integration.py`) проверяли суммы/источник напрямую на теперь скрытых INPUT-строках листа AHR. Переведены на сравнение через заголовок статьи (ITEM/SUB_ITEM/SUB_SUB_ITEM — глубина статьи в дереве расходов разная для разных категорий, проверено эмпирически) по схеме «до/после патча» (устойчиво к существующим сидовым данным), либо через лист-источник, где детализация не пряталась. ## Тесты Интеграционные (`api/tests/integration/test_form2_ahr_satellite_integration.py`, общий файл серии задач 1907/1909/1887) — 10 новых тестов на все пункты приёмки (скрытие лимитных LEAF с верной суммой на заголовке, скрытие записи аренды/коммуналки/охраны с видимостью на листе-источнике, регрессия на ручную «сверх лимита» строку, `can_add_row=false/true`, регрессия на обычную статью). Плюс адаптация 3 ранее существовавших тестов, сломанных скрытием строк (см. R-4). Фронт: `canAddRowForHeader` (чистая функция в `utils/rowUtils.js`) покрыта `node --test` (`rowUtils.test.js`). **Одноразовая Postgres 16 в Docker** (не `perf-pg`, снесена после работы). `cd api && pytest`: 247 passed, 3 failed (прежний известный набор, не связанный с этой веткой: `test_backend_calls_form3_add_project_and_add_line_functions`, `test_export_full_form_ignores_non_applicable_params_for_other_sheets`, `test_export_form_payload_forwards_ignore_flag`), 4 skipped — итого 254 (было 244 до +10 новых). `cd web && node --test src/components/RealtimeTable/utils/*.test.js`: 15 passed. ## Test plan - [x] TDD: падающий тест → реализация → зелёный прогон - [x] Полный `api/pytest`: разница — ровно +10 новых тестов, прежние 3 известных падения без изменений - [x] Фронтовые `node --test`: 15/15 зелёных - [x] `perf-pg` не затронут (использовалась отдельная одноразовая Postgres 16, снесена) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
aep added 6 commits 2026-10-31 21:00:13 +03:00
Добавлен столбец «СПОД» между «Декабрь» и «Итог 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>
Прогнал api/tests/integration/test_form2_ahr_satellite_spod_integration.py
против живого Postgres (perf/perf/perf на localhost:55432, БД dfip_1906,
alembic upgrade head + фикстура) — нашёл и поправил два бага самого теста
(фичу/миграцию не трогал):

- _find_row не учитывал служебную META-строку (editable/role/user_id без
  id/header), которую v3.v_form_view добавляет первой в любой лист —
  KeyError: 'id' на первой же выборке. Теперь ищет только среди INPUT-строк.
- test_satellite_spod_phase_gates_non_admin_user делал один тест из
  "запрет без fact_q4.spod → добавили ключ → разрешено": неадминский
  403/400 от isp1 внутри теста вызывает db_ref.rollback() в
  tests/integration/conftest.py (per-request откат на ошибке), который
  стирает все несохранённые изменения этого же теста — включая только
  что созданные строку сателлита и form_phase. Разбил на два независимых
  теста (test_satellite_spod_rejected_before_phase_includes_spod_column /
  test_satellite_spod_allowed_once_phase_includes_spod_column), в каждом
  ровно один запрос, которому разрешено провалиться.

pytest tests/integration — 157 passed, 1 failed (pre-existing,
не связан: test_backend_calls_form3_add_project_and_add_line_functions,
FORM_3/add_project, 400 на /api/v1/projects/.../report/.../line), 4 skipped.
Новый файл — 15/15.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v3.add_budget_line (ветка сателлитов AHR_RENT/AHR_UTILITY/AHR_SECURITY)
искала любую существующую строку статьи без ORDER BY и без исключения
ручных/уже привязанных строк, и переиспользовала её как заготовку для
новой записи. Если у статьи ещё не было строк в
v3.form2_ahr_satellite_line_source (типично — первая запись при наличии
ручной строки той же статьи), sync переименовывал и перезаписывал план/факт
чужой ручной строки, блокируя её для правки.

Теперь для каждой новой записи сателлита всегда создаётся отдельная
пустая строка-заготовка — sync забирает её как строку именно этой записи.
Заодно удалён мёртвый код (_ensure_form2_ahr_sync_line,
_upsert_form2_ahr_sync_values), не имевший вызывающих мест.

AURORA-1907 (лимитная часть) и AURORA-1887 (статьи 314/24/779 в коде) —
отдельные задачи, не затронуты.

Closes #265.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AURORA-1897: лист «АХР лимиты» формы 2 дублировал годовой норматив ДМС/оргтехники
в каждом квартале вместо деления на 4, считал сумму от лимита с НДС (смета
ведётся без НДС) и переносил рубли в план АХР, где всё остальное в тыс. руб.

Миграция 0052_form2_ahr_limit_period_vat_units:
- v3.limit_template.period (YEAR/QUARTER/MONTH) + точечные правки справочника
  (id 24/25 ДМС и id 13-15 принтеры/МФУ/копиры — YEAR; id 7/8 SIM-карты —
  заполнен limit_without_vat; id 21 — unit по аналогии с бумагой).
- v3._form2_ahr_limit_amount(period, limit, qty) — рубли без НДС, без
  проверки по названию; старая 4-арг. сигнатура удалена.
- v3.v_form2_view (ветка AHR_LIMIT), v3.sync_form2_ahr_satellite_lines(_line)
  пересозданы: amount/plan считаются от limit_without_vat, план АХР делится
  на 1000, добавлены amount_year и строка «Итого» (id=0, row_type ROOT).
- v3._limit_template_path_set дописывает id=0 в path-set, чтобы «Итого»
  попадала в diff PATCH-ответа.
- Пересинхронизированы все существующие формы 2.

Ветки аренды/коммуналки/охраны и add_budget_line (AURORA-1887/1909) не тронуты.

Тесты: api/tests/integration/test_form2_ahr_limit.py (7 тестов, числа из
тикета: id24 qty10→69645/69.645, id3→2000/2.0, id7→7500/7.5, id13→6471.875/
6.471875, id17→7500/7.5, «Итого», регрессия АХР_RENT без деления).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Миграция 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, расходится с архитектурным
предложением из тикета): без слияния 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>
This pull request can be merged automatically.
You are not authorized to merge this pull request.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/272-aurora-1887-ahr-hide-satellite-rows:feat/272-aurora-1887-ahr-hide-satellite-rows
git checkout feat/272-aurora-1887-ahr-hide-satellite-rows
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RSHB/DFiP_Budget_planing#273
No description provided.