R-4 (решение пользователя 08.11). В миграцию 0056 добавлен v3.add_budget_line:
тело = 0051_form2_ahr_satellite_new_line + только created_by = current_user_id() в
двух INSERT (обычная строка и заготовка записи сателлита). Авто-зеркальные строки
sync (_ensure_form2_ahr_*_sync_line) остаются с NULL. Backfill не делается.
del_budget_line: NULL-автор -> удаляет только ADMIN (комментарий в SQL + тесты).
xfail-тест владельца снят; добавлены тесты NULL-автора (исполнитель 403, ADMIN 200).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Миграция 0050_budget_line_delete_owner_guard -> 0056 (down 0055, стек поверх 0055 AURORA-1938).
Тело v3.del_budget_line = актуальное тело из 0053_form2_ahr_source_sheet (AURORA-1907:
запрет удаления АХР-строк с source_sheet IS NOT NULL, ручные удаляются) + только owner-guard;
раньше копия опиралась на 0040 и откатывала 1907.
Тесты: доступ к форме id=1 даёт отдельная пара исполнителей isp2/isp3 (isp1 больше не
добавляется в org 1 — это ломало тесты «исполнитель из чужой организации»); окно фазы
DFIP в фикстуре продлено (истекло 2026-06-06, add_line возвращал 403); маркер
internal_order не приходит в INPUT-строке add_line — берём единственную INPUT-строку;
добавлен тест 1907 (строка из сателлита не удаляется напрямую). Тест «владелец удаляет свою
строку» помечен xfail(strict): add_budget_line не заполняет budget_line.created_by (O-1).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Конфликт 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>
- конфликт 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>
Конфликт в api/tests/integration/test_form_phases_api_smoke.py: оставлены
оба набора тестов (тесты фильтра по роли и 404 из этого PR и тест
aware-дат из test).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Миграция 0050_form4_transfer_econ_calc -> 0055 (down 0054): номер 0050 занят
на test. Тело v3._apply_form_cell = актуальное тело из 0050_form2_ahr_spod_column
+ только дельта AURORA-1938 (запрет ручной записи FORM_4 q*.transfer_econ);
раньше копия опиралась на 0023 и откатывала СПОД. v_form4_sheet_sections
после 0033 на test не менялась — тело без изменений. Добавлен тест на сохранение
обеих правок в итоговом определении функции.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR #292 слил два независимо развивавшихся набора миграций 0050-0053
(цепочка Фомичева и ветка AURORA-1850/upd_form_cells_subtree) простой
конкатенацией SQL. Для 0052 это дало ВТОРОЕ, более старое определение
v3.v_form2_view (без СПОД и без правок лимитов AURORA-1897/1906) ниже
первого в том же файле — оно и остаётся финальным телом функции.
Аналогичная проблема в 0054: v3.v_form2_sheet_jsonb там 3-аргументная,
без p_focus_lines, поэтому реальный вызов из v_form2_view (4 аргумента)
резолвился в версию из 0052 без AURORA-1887/1907.
- 0052: убрано второе определение v_form2_view; AHR_LIMIT-ветка и
fact_q4.spod в AHR_RENT/AHR_UTILITY/AHR_SECURITY перенесены в
единственное (4-арг.) тело функции; строка «Итого» теперь не
возвращается при непустом p_focus_lines без совпадений (пустая
дельта остаётся пустой).
- 0054: v_form2_sheet_jsonb переведена на 4-арг. сигнатуру с DROP
старой 3-арг. перегрузки.
- 0053: get_form2_ahr_satellite_delta — добавлен SUB_SUB_ITEM в список
заголовков (без него дельта по статьям глубины 4 пуста).
Closes issue #294.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Экран: {unit}-плейсхолдер (formatHeaderUnit, переиспользует разрядность из
useDenomination/moneyUtils AURORA-1899) в TableHead.jsx, CollapsibleTree.jsx,
templateColumns.js; литерал «тыс. руб.» → {unit} в заголовках FORM_1/FORM_2/
FORM_4/PROJECT (кроме AHR_LIMIT.js — рублёвое исключение не трогаем) и в
своих колонках Свод1/Свод2 (SummaryTable.scaleColumns, Svod2Page.
scaleNumericColumns). Текст НДС на экране не менялся.
Выгрузка: «с учетом НДC» (латинская C, 96 мест в FORM1_DEPTH_COLUMNS и
производных FORM4_*) → плейсхолдер {vat}/{vat_plan}, резолвится по листу
через render_column_years(..., extra=...) — Ф.1 OPER/CAP «с учётом НДС»,
Ф.1 AHR и все листы Ф.4 «без НДС» (соответствует экрану). SMETA_COLUMNS —
«, тыс. руб.» у сумм. Своды — строка метаданных «Единица: тыс. руб.».
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
До этой правки лист "Смета" всех форм выгружался одной плоской
раскладкой 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>
AURORA-1848 (#239/#242) убрал null-поля из GET листа/сводов (jsonb_strip_nulls),
оставив PATCH как раньше (задокументированный breaking-change в PR, не баг).
После слияния веток в test это разошлось с тестами, написанными до #239/#242:
- sheet_contract.assert_patch_rows_match_sheet сравнивал PATCH и GET побайтно —
добавлен strip_nulls() на стороне PATCH перед сравнением.
- test_form4_sheet_sums.py сравнивал ответ API с "сырым" v3.jsonb_sum (с null-полями)
и ожидал явный null в некоторых полях ответа — эталон теперь тоже прогоняется через
strip_nulls, а прямые ожидания null заменены на .get(...) is None / отсутствие ключа.
Без изменений контракта API: PATCH продолжает отдавать null явно, как в #242.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
После 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>
row_json_select(strip_nulls) оборачивает data в jsonb_strip_nulls внутри json_build_object;
флаг пробрасывается через get_json/get_svod1_json/get_svod2_json, роутеры листа и сводов
включают его. Проекты, PATCH/POST/DELETE не затронуты.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#229, #231 и #244 уже заняли 0050-0052 к моменту слияния этого PR.
Переносим form4_sheet_sum на 0053, down_revision=0052, и правим путь
к SQL-файлу в perf/analysis/explain_manual.py.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
При слиянии PR #244 в test оказались две миграции с revision="0051"
и down_revision="0050" (upd_form_cells_full_rows из #231 и
upd_form_cells_subtree из #244) — неоднозначная цепочка alembic.
Фикс-коммит с переименованием 0051→0052 для #244 был подготовлен
локально, но потерялся при повторном force-push после блокировки
классификатором permission — слияние прошло с исходной (дублирующейся)
нумерацией. Переносим upd_form_cells_subtree на 0052.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Три независимых approved PR (#229, #231, #232) добавляли миграцию
с одним и тем же revision=0050 поверх общего down_revision=0049.
#229 слит первым и занял 0050 (project_technical_number_lock),
поэтому здесь переносим upd_form_cells_full_rows на 0051.
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>
Миграция 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>
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>