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>
Прогнал 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>
Добавлен столбец «СПОД» между «Декабрь» и «Итог 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>
Перенос в фонд экономии (q1/q2/q3/q4.transfer_econ, столбцы 75/107/137
и их Q4-аналог формы РФ) был raw passthrough-полем budget_line_quarter.
transfer_to_economy: ничего его не считало, поэтому после сохранения
строки с планом без переносов поле оставалось пустым, а "ВСЕГО" блоков
показывало прочерк вместо суммы.
v3.v_form4_sheet_sections теперь считает transfer_econ_QN из уже
имеющегося residual_after_actual_QN с поправкой на то, что тот уже
вычитает перенос в СЛЕДУЮЩИЙ квартал один раз (через cp1/cp2/cp3):
transfer_econ_Q1 = residual_after_actual_Q1 - transfer_q3 - transfer_q4
transfer_econ_Q2 = residual_after_actual_Q2 - transfer_q4
transfer_econ_Q3 = residual_after_actual_Q3
transfer_econ_Q4 = residual_after_actual_Q4
Буквальный перенос формулы Excel (ещё и минус transfer_q2 для Q1 /
transfer_q3 для Q2) задвоил бы вычитание. АХР, КВ (CAP) и Операц (OPER)
используют одну и ту же функцию, поэтому один фикс закрывает все три
листа; STRUCTURE — агрегирующий read-only свод без INPUT-строк, его не
касается.
v3._apply_form_cell для FORM_4 больше не принимает прямую запись в
q*.transfer_econ (ValueError computed_field, как у residual_after_actual).
Тесты (api/tests/integration/test_form4_transfer_econ.py):
- репро тикета (переносы = 0);
- регрессия на двойное вычитание с ненулевыми transfer_q2/q3/q4,
отличающая верную формулу от наивного Excel-порта;
- КВ и Операц дают те же числа, что АХР;
- запись в transfer_econ отклоняется.
Closes#255.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- 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>
Выгрузка 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>
Листы Операц/АХР/КВ формы «Смета РФ» (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>
Удаление строки в формах 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>
Фикстура свода 2 достроена до 6 строк на GO и RF по двум орг. единицам с разными section_code;
все случаи SVOD2_CASES проверяют непустоту. Тест rf-rollup переименован (фиксирует существующий дефект),
добавлен тест gzip, в api_split.py оставлены только GET-места.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Добавлены list_json_response (обёртка BaseListResponse из готовых строк),
ROW_COLUMNS, row_json_select и fetch_row_texts для тяжёлых GET (AURORA-1848).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AURORA-1845. process() больше не читает user_service.get / bf_service.get на
каждое сообщение: результат кэшируется в ConnectionCache (TTL 30 с, только
положительные результаты).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Выборка строк вынесена в v3._form_affected_rows; FORM_4 отбирает ROOT, GROUP и ITEM только изменённых строк; при sections убираются ключи исключённых разделов.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
asyncpg не принимает aware-datetime для CAST AS TIMESTAMP WITHOUT TIME ZONE (400).
Даты переводятся в пояс оргединицы и становятся naive, SQL приписывает тот же offset.
timezone_from_offset вынесен в src/core/timezones.py, добавлен to_org_local_naive.
Aware-дата при NULL utc_offset оргединицы даёт 422.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SQLAlchemy не объявляет greenlet без extra asyncio. На arm64 (macOS)
и на Python >= 3.13 greenlet не ставится, и async-движок падает с
"the greenlet library is required". Тест фиксирует наличие greenlet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UserInDB наследует новую UserReadBase (email/username/full_name без
валидации формата). Вход (UserBase/UserCreate/UserUpdate) не менялся.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
На PostgreSQL 15 подзапрос в FROM без алиаса не принимается, миграция
падала. Добавлен алиас active_reports, смысл не меняется. Правка старой
миграции: 0012 выполняется раньше любой новой, на базах, где 0012 уже
применена, ничего не меняет.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>