From feee68d4e5c0108ac6f5875a8f79f4f49b3a749d Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 20:25:51 +0400 Subject: [PATCH 01/20] =?UTF-8?q?docs(openspec):=20aurora-1850-upd-form-ce?= =?UTF-8?q?lls-subtree=20=E2=80=94=20proposal,=20spec,=20design?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../.openspec.yaml | 2 + .../design.md | 246 ++++++++++++++++++ .../proposal.md | 47 ++++ .../specs/sheet-cell-edit/spec.md | 114 ++++++++ 4 files changed, 409 insertions(+) create mode 100644 openspec/changes/aurora-1850-upd-form-cells-subtree/.openspec.yaml create mode 100644 openspec/changes/aurora-1850-upd-form-cells-subtree/design.md create mode 100644 openspec/changes/aurora-1850-upd-form-cells-subtree/proposal.md create mode 100644 openspec/changes/aurora-1850-upd-form-cells-subtree/specs/sheet-cell-edit/spec.md diff --git a/openspec/changes/aurora-1850-upd-form-cells-subtree/.openspec.yaml b/openspec/changes/aurora-1850-upd-form-cells-subtree/.openspec.yaml new file mode 100644 index 0000000..1bb6456 --- /dev/null +++ b/openspec/changes/aurora-1850-upd-form-cells-subtree/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-28 diff --git a/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md b/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md new file mode 100644 index 0000000..fac0162 --- /dev/null +++ b/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md @@ -0,0 +1,246 @@ +# Design: правка ячейки пересчитывает только затронутое поддерево (AURORA-1850, #235) + +Зачем это нужно — в proposal.md, раздел Why. Требования — в specs/sheet-cell-edit/spec.md. + +## Context + +Где мы сейчас: ветка стоит в стеке от `feat/215-aurora-1844-upd-form-cells-full-rows`, миграция 0050. + +- Цепочка вызовов при правке такая. HTTP `PATCH .../cell(s)` или WS `cell_updated` попадает в `SheetRepository.update_cell(s)` (`api/src/repository/sheet_repository.py:70-116`). Оттуда вызывается `v3.upd_form_cell` (`0002_functions.sql:2478`, SQL-обёртка). Далее — `v3.upd_form_cells(..., p_user_id, ...)`: права, `computed_field`, синхронизация сателлитов (`0031_form2_ahr_satellite_sync.sql:904-987`). После неё — `v3.upd_form_cells` без пользователя (`0050_upd_form_cells_full_rows.sql`): проверка строк, `_apply_form_cell`, затем один вызов `v3._form_affected_rows(p_form_id, p_sheet, p_ids, p_direction, p_sections)`. +- `_form_affected_rows` (0050) во всех ветках собирает весь лист через `v3.v_form_view` и фильтрует результат. Фильтры по листам: + - сателлиты — `data.id ∈ p_ids`; + - AHR_LIMIT — путь по `limit_template` (`_limit_template_path_set`); + - OTCH9F — то же `(expense_item_id, equipment_name)`; + - FORM_4 — ROOT, GROUP по программе и ITEM по проекту; + - FORM_1 и FORM_2 AHR/CAP/OPER — агрегаты с `header.expense_item_id ∈ _expense_item_path_set(eid изменённых строк)` плюс INPUT с `line_id ∈ p_ids`; + - при `p_sections` ключи исключённых разделов вырезаются (`v_strip`). +- Сборка FORM_2 AHR/CAP/OPER идёт по цепочке `v_form_view` → `v_form2_view` (`0033:1-373`, переписана регуляркой в 0037) → `v_form2_sheet_jsonb` (`0015:2534-2804`, LANGUAGE sql, проекция строка в строку) → `v_form2_sheet_sections` (`0033:469-1155`, plpgsql). Внутри `v_form2_sheet_sections`: + - `tw` — все узлы дерева статей листа для типа формы; + - `page` — все строки формы на листе; + - `jp..jq4` и `input_base` — колонки INPUT; + - `agg` и `aq_q` — суммы по `expense_item_id` по всем строкам листа; + - `tw_agg` и `tw_aq1..4` — суммы узла: `LEFT JOIN agg ON eid = ANY(tw.desc_ids)`, причём `desc_ids` включает сам узел (`0001_initial_schema.py:1063-1065`); + - `_sort`: для INPUT это `tree_path || line_id`, для узла — `t.path`. +- `sort_order` вычисляется выше по цепочке: `ROW_NUMBER() OVER (ORDER BY _sort_path)` в `v_form2_view` (`0033:21`), для FORM_1 — в ветке `v_form_view` (`0015:1478-1507`). Набор ключей полного листа — все узлы `tw` плюс строки `page ⋈ tw`. Соединение внутреннее, поэтому строка статьи, не привязанной к типу формы, в лист не попадает. +- FORM_1 устроена так же: `v_form_view` (ветка AHR/CAP/OPER) → `v_form1_sheet_jsonb` (`0015:2268`) → `v_form1_sheet_sections` (`0033:1723-2470`). Отличие в том, что `page` и все агрегатные CTE фильтруются по `direction` (`bl.direction IS NULL OR = p_direction`), а `tw` — нет. +- Сателлитные листы, AHR_LIMIT и OTCH9F собираются прямо в `v_form2_view`. `sort_order` у них такой: + - RENT/UTILITY/SECURITY — `ROW_NUMBER` по `id`; + - AHR_LIMIT — `limit_template.sort_order`; + - OTCH9F — `ROW_NUMBER` по `(expense_item_id, month)`. +- `v3.get_form2_ahr_satellite_delta` (0031:784-855, переписана регуляркой в 0037) вызывается в фоне из `notify_ahr_about_satellite_change` (`websocket.py:798-903`). Она собирает `v_form_view(form, 'AHR', NULL, 'Support')` целиком и фильтрует ROOT/GROUP/ITEM/SUB_ITEM по пути, а SUB_SUB_ITEM не берёт. Результат уходит на лист AHR как `cell_updated` или `row_added`. +- Где тратится время (`perf/report-data/20261026-s1/explain-manual/summary.md`, `form2_ahr_tw.jit_off`; 2091 строка): + - тело `v_form2_sheet_sections` — 240 мс; + - основное — 4 × `tw ⋈ aq_q` вложенным циклом (546 узлов × 438 групп, около 100 мс); + - соединения `input_base` (около 60 мс); + - JIT по умолчанию добавляет ещё 210–230 мс (М12). +- Фронт ищет строку ответа по `(row_type, depth, sort_order)` (`cellUtils.js:76`; так же в ветке AURORA-1838). Поэтому неточный `sort_order` молча испортит чужую строку. + +## Goals / Non-Goals + +**Goals:** +- Правка FORM_1 и FORM_2 (все редактируемые листы) не собирает лист целиком. Ответ по составу и содержимому байт в байт равен тому, что дала бы полная сборка с фильтром из 0050. +- Один источник истины. Суммы, колонки INPUT и `sort_order` считаются тем же кодом, что и GET. +- Фоновое обновление АХР после правки сателлита не собирает AHR целиком. + +**Non-Goals:** +- FORM_4: остаётся на полной сборке до слияния AURORA-1846. +- `add_budget_line` и `del_budget_line`: их ответы по-прежнему строятся полной сборкой. +- Ускорение GET листа. +- Инкрементальные или материализованные суммы. +- Пакетная обработка правок соединения (бывшая часть AURORA-1845). +- Кэш пользователя и формы на соединение (AURORA-1845). +- Изменения фронта. + +## Решения (согласовано 28.10) + +1. `v3._form_affected_rows` переписывается так, чтобы не собирать весь лист. Существующие построители (`v_form1_sheet_sections`, `v_form2_sheet_sections` и вызывающие их функции) получают параметр фокуса по строкам. NULL означает прежнее поведение. Параллельной копии нет. `sort_order` в ответе точный, то есть глобальный `ROW_NUMBER`. +2. Охват — только FORM_1 и FORM_2: AHR/CAP/OPER, сателлиты, AHR_LIMIT и OTCH9F (способ — по дизайну ниже). FORM_4 остаётся на полной сборке до AURORA-1846 (PR #232), критерий для FORM_4 переносится. +3. `get_form2_ahr_satellite_delta` переводится на тот же механизм. Тело берётся с мигрированной БД, с учётом 0037. +4. Корректность доказывает тест эквивалентности на базе `sheet_contract.py`: каждая строка ответа PATCH побайтно равна строке того же `sort_order` из полного GET. Покрываются FORM_1 и FORM_2 (все листы), первая и последняя строка, очистка, PATCH `/cells` по разным ветвям, `sort_order` после POST и DELETE line. +5. Критерий: mean `upd_form_cell` ≤ 50 мс на FORM_2/AHR и 0 вложенных полных сборок листа на правку. Вторая точка снимается после grow (2092 строки). +6. AURORA-1845 сужена до кэша на соединение. После 1850 ручной ввод не должен создавать очередь — это проверяется замером coauthor на rate 5. +7. Номер миграции — 0051, в стеке после 0050 из 1844. При rebase на 1831 и 1846 номер меняется. + +## Decisions + +### D1. Параметр фокуса и его семантика +У `v_form2_sheet_sections`, `v_form2_sheet_jsonb`, `v_form2_view`, `v_form1_sheet_sections` и `v_form1_sheet_jsonb` последним параметром добавляется `p_focus_lines integer[] DEFAULT NULL`. В новой `v_form1_view` (D3) он тоже есть. +- `NULL` — полный лист, поведение прежнее. +- Непустой массив — только строки фокуса и узлы их путей. +- Пустой массив `'{}'` — **пустой результат**, ни в коем случае не полный лист. `array_agg` по пустому набору даёт NULL, поэтому вызывающие обязаны передавать `COALESCE(..., '{}')`. Это проверяется тестом (раздел «Тестирование», п. 6). + +Смысл фокуса для листов AHR/CAP/OPER: +- **Узлы фокуса** — `_expense_item_path_set(eid строк фокуса)` ∩ `tw`. eid берутся из `budget_line` **без** фильтра `direction`, как в 0050: правка строки чужого направления в FORM_1 возвращает предков, но не саму строку. +- **INPUT фокуса** — строки фокуса, прошедшие обычный фильтр `page` (форма, лист, `direction`, соединение с `tw`). +- **Суммы.** `agg` и `aq_q` (у FORM_1 — `agg`, `aq1..aq4`, `atot`) ограничиваются строками с `expense_item_id ∈ ∪ desc_ids(узлы фокуса)`. Сумма узла определяется только его `desc_ids`, которые входят в это объединение, поэтому значение не меняется. `tw_agg` и `tw_aq*` строятся только по узлам фокуса. +- `jp..jq4`, `input_base` и `final_input` строятся только по INPUT фокуса. + +Отвергнутые варианты: +- (а) Отдельная «лёгкая» функция сумм, копия формул. Отвергнута решением человека: два источника истины. +- (б) Фильтр поверх полного `v_form_view`. Это текущее состояние: plpgsql-функция не принимает `WHERE` внутрь. +- (в) Ветвление `IF p_focus_lines IS NULL THEN <запрос> ELSE <копия запроса>` внутри построителя. Это та же копия на 700 строк. + +### D2. sort_order считает построитель, по «скелету» +Построители `*_sheet_sections` и `*_sheet_jsonb` получают выходную колонку `_sort_rank bigint`. Её значение — `ROW_NUMBER() OVER (ORDER BY _sort, )` по **скелету** листа. Скелет — ключи всех узлов `tw` и всех строк `page ⋈ tw` без jsonb и без сумм: ровно тот набор ключей, который даёт полный лист. Ранг считается одинаково в обоих режимах, и строки вывода получают его соединением по ключу. + +`v_form2_view` и `v_form1_view` берут `sort_order` из `_sort_rank` и больше не считают собственный `ROW_NUMBER`. + +Скелет на 2092 строки стоит единицы миллисекунд: сортировка около 2,7 тыс. `int[]`. Это лишняя работа и для GET, но зато порядок считается одним кодом в обоих режимах. + +Отвергнутые варианты: +- (а) `ROW_NUMBER` в вызывающих функциях, как сейчас. В режиме фокуса он нумерует подмножество. +- (б) Отдельный запрос `count(*) ключей < k` в `_form_affected_rows`. Он дублирует правила набора ключей (`direction`, соединение `page ⋈ tw`). + +### D3. FORM_1: новая `v3.v_form1_view` +Ветка FORM_1 AHR/CAP/OPER в `v_form_view` (`0015:1478-1507`, `jsonb_build_object` из 16 ключей) выносится в `v3.v_form1_view(p_form_id, p_sheet, p_sections, p_direction, p_focus_lines)`. Она симметрична `v_form2_view`. `v_form_view` вызывает её с фокусом NULL, `_form_affected_rows` — с `p_ids`. Сигнатура `v_form_view` не меняется (`CREATE OR REPLACE`). + +Отвергнутый вариант: `_form_affected_rows` сама собирает jsonb поверх `v_form1_sheet_jsonb`. Тогда у проекции строки было бы два места. + +### D4. Сателлиты, AHR_LIMIT, OTCH9F — внутри `v_form2_view` с тем же параметром +- **AHR_RENT, AHR_UTILITY, AHR_SECURITY.** Ранг — `ROW_NUMBER() OVER (ORDER BY id)` по всем строкам формы на этом листе: только `id`, без соединения с `vsp` и без jsonb. Затем фильтр `id = ANY(p_focus_lines)`, и jsonb строится только для отобранных строк. Здесь `p_focus_lines` — id деталей (как `p_ids` в 0050). +- **AHR_LIMIT.** Вывод фильтруется по `_limit_template_path_set(p_focus_lines)`. `sort_order` = `lt.sort_order` — статичный, от фокуса не зависит. Агрегат `tree`/`leaf_amt`/`agg` остаётся по всему шаблону: шаблон общий и его размер не зависит от числа строк формы. Сужать его можно, но не обязательно (на усмотрение исполнителя). Формула 0037 (`v3._form2_ahr_limit_amount`) сохраняется. +- **OTCH9F.** `base` ограничивается разделами `(expense_item_id, equipment_name)` строк фокуса. Окна разделяются по `(budget_form_id, expense_item_id, equipment_name)`, поэтому разделы целые и остатки те же. Ранг — `ROW_NUMBER() OVER (ORDER BY expense_item_id, month, id)` по всем строкам формы (D6) на лёгком скелете с тем же `JOIN expense_item`. +- **SMETA и прочие листы** — без изменений: фокус игнорируется, а `_form_affected_rows` для них по-прежнему зовёт полный путь (такие листы не редактируются). + +### D5. `_form_affected_rows` и `get_form2_ahr_satellite_delta` +- `_form_affected_rows`, ветки FORM_1 и FORM_2: + - AHR/CAP/OPER → `v_form1_view` или `v_form2_view` с `p_focus_lines = p_ids`; + - сателлиты, AHR_LIMIT, OTCH9F → `v_form2_view` с фокусом. + + Правила состава ответа (0050) при этом выполняет сам построитель. `v_strip` и `ORDER BY sort_order` остаются. Ветка FORM_4 без изменений. Функция помечается `STABLE`: только чтение, а её вызывает STABLE-функция дельты. +- `get_form2_ahr_satellite_delta`: CTE `target_lines` сохраняется **в том виде, как он стоит в БД после 0037**, вместе с сопоставлением `(ei.depth = 3 AND ei.item_id = …) OR ei.num_group_id = …`. Финальный `SELECT` заменяется на `SELECT * FROM v3._form_affected_rows(p_form_id, 'AHR', COALESCE((SELECT array_agg(line_id) FROM target_lines), '{}'), 'Support', NULL)`. CTE `target_path` больше не нужен. Сигнатура и `LANGUAGE sql STABLE` прежние, Python не меняется. +- Изменение поведения дельты: теперь в неё входят предки уровня SUB_SUB_ITEM, а раньше фильтр их пропускал (0031:843). После 0033 у AHR есть статьи глубины 4, и их агрегат у соавторов оставался старым. Это исправление по контракту «все предки». + +### D6. Детерминированный порядок при совпадении ключа +Ключ INPUT `tree_path || line_id` совпадает с `path` дочернего узла, если `line_id` равен id дочерней статьи, а строка привязана к нелистовой статье. На AHR такое бывает: после 0033 у статей глубины 3 есть дети глубины 4, и строки синхронизации сателлитов привязаны к статьям глубины 3. Сейчас порядок при таком совпадении зависит от плана, а скелет и полный вывод могли бы упорядочить строки по-разному. + +Добавляется tie-break `ORDER BY _sort, line_id NULLS FIRST`: при совпадении узел идёт раньше строки. Для OTCH9F добавляется `id` после `(expense_item_id, month)`. GET меняется только там, где порядок раньше был не определён. + +### D7. JIT выключается в построителях и на пути правки +`SET jit = off` ставится как атрибут функций `_form_affected_rows`, `get_form2_ahr_satellite_delta`, `v_form2_sheet_sections` и `v_form1_sheet_sections`. Причины две. +1. Запрос с фокусом в custom-плане может оценить стоимость выше `jit_above_cost` и потратить 200+ мс на компиляцию. Это больше всего бюджета в 50 мс. +2. План кэшируется один на оператор, общий для GET и правок в одном backend. Дешёвые custom-планы правок снижают среднюю стоимость custom-плана, и после этого GET может перестать переходить на generic-план. Тогда GET будет каждый раз платить JIT (+210–230 мс, М12). + +По М12 JIT на этих запросах только вредит: 534 мс против 240 мс. Атрибут `SET` не мешает: plpgsql не инлайнится. + +Отвергнутые варианты: +- (а) `jit = off` только на пути правки. Не закрывает причину 2. +- (б) `plan_cache_mode = force_custom_plan`. Добавляет 7–8 мс планирования на каждую правку и усиливает причину 2. +- (в) Глобальная настройка Postgres. Вне репозитория, прод не наш. + +### D8. Тела функций в миграции — буквальные, снятые с мигрированной БД +Тела `v_form2_view` и `get_form2_ahr_satellite_delta` берутся из `pg_get_functiondef` на БД, мигрированной до 0050, и правятся вручную. Регулярные выражения поверх `pg_get_functiondef`, как в 0037, не используются. + +Отвергнутый вариант — DO-блок с `regexp_replace`. Правка затрагивает десятки мест, а регулярка в 0037 уже показала хрупкость (ветка `CONTINUE`). + +Защита от отката 0037 — тесты из раздела «Тестирование», п. 7. + +### D9. Порядок операторов в миграции 0051 +Старые сигнатуры удаляются явно, **без CASCADE**. Если обнаружится скрытая зависимость, миграция упадёт, а не удалит лишнее. Порядок: +1. `DROP FUNCTION v3.v_form2_view(integer, character varying, text[])`, затем `v3.v_form2_sheet_jsonb(integer, character varying, text[])`, затем `v3.v_form2_sheet_sections(integer, character varying, text[])`. +2. Затем `v3.v_form1_sheet_jsonb(integer, character varying, character varying, text[])` и `v3.v_form1_sheet_sections(integer, character varying, character varying, text[])`. +3. `CREATE` в порядке «вызываемая раньше вызывающей», потому что тела LANGUAGE sql проверяются при создании: + - `*_sheet_sections`; + - `*_sheet_jsonb`; + - `v_form2_view`, `v_form1_view`; + - `v_form_view` (`CREATE OR REPLACE`); + - `_form_affected_rows` (`CREATE OR REPLACE`); + - `get_form2_ahr_satellite_delta` (`CREATE OR REPLACE`). + +Без DROP функции с DEFAULT-параметрами дали бы неоднозначные перегрузки: вызов `v_form2_view(a, b, c)` из `v_form_view` подходит к обеим. + +Ограничения формата: +- `api/alembic/sql_migration.py` пропускает строки, начинающиеся с `--`, и ищет `$tag$` построчно. Открывающий и закрывающий тег должны стоять на разных строках, закрывающий — в начале строки, как в существующих файлах. +- SQL совместим с PostgreSQL 15.12: без `any_value`, у подзапросов в FROM есть алиасы. + +Файлы: `api/alembic/versions/0051_upd_form_cells_subtree.py` (`down_revision = "0050"`, `run_sql_file_migration`) и `api/alembic/versions/sql/0051_upd_form_cells_subtree.sql`. `downgrade()` пустой, как во всех миграциях. + +## Risks / Trade-offs + +- **Суммы предков в режиме фокуса разойдутся с полной сборкой.** Защита: параметризация вместо копии (D1); побайтная эквивалентность на всех строках тестовых листов («Тестирование», п. 1–2); та же проверка на данных стенда («Замер», шаг 4). +- **`sort_order` разойдётся с полным листом, и фронт молча испортит соседнюю строку.** Защита: ранг по скелету одним кодом (D2), tie-break (D6), тесты после POST и DELETE line и тест на совпадение ключа. +- **Пустой фокус превратится в полный лист** (NULL из `array_agg`). Последствия: дельта АХР отдаст весь лист, правка снова станет дорогой. Защита: семантика `'{}'` (D1), `COALESCE` в вызывающих, тест п. 6. +- **Откатится правка 0037** (формула лимитов, сопоставление статей глубины 4). Защита: D8 и тесты п. 7. +- **Регресс GET** из-за общего кэша планов, лишнего скелета или выключенного JIT. Защита: D7; требование «GET листа не меняется»; замер GET до и после со смешанной нагрузкой («Замер», шаг 5). +- **Generic-план для запроса с фокусом окажется плохим.** Фильтры фокуса стоят в базовых CTE (`tw`, `page`, `agg`/`aq_q`), которые материализуются, поэтому объём ниже по плану мал при любом плане. Если замер покажет обратное, следующий шаг — `SET plan_cache_mode = force_custom_plan` только на `_form_affected_rows`, с повторным замером. +- **Сумма ROOT растёт с размером листа**: агрегат предка суммирует все строки под ним. Числовой `SUM` без jsonb на 2092 строках, по оценке, укладывается в 10–20 мс, а вложенные циклы `tw ⋈ aq_q` (около 100 мс) сужаются до 4–5 узлов. Если вторая точка (2092 строки) превысит 50 мс, исполнитель останавливается и эскалирует с профилем EXPLAIN: инкрементальные суммы — это уже другая задача. +- **Права, аудит, синхронизация сателлитов** не затронуты: это слой выше `_form_affected_rows`. +- **Зависимости.** AURORA-1844 — шов и контракт, это база ветки. AURORA-1846 нужна только для будущего фокуса FORM_4. Номер миграции может конфликтовать с 1831 и 1846. + +## Migration Plan + +- PR идёт в ветку `feat/215-aurora-1844-upd-form-cells-full-rows` (стек, Gitea #235). После слияния 1844 в `test` ветка перебазируется на `test`. +- **Перенумерация.** Если до слияния стека в `test` попадут 1831 (`0050_project_technical_number_lock`) или 1846 (`0050_form4_sheet_sum`), 1844 получит следующий свободный номер, а 1850 — номер после него. При rebase меняются имена файлов, `revision`, `down_revision` и ссылка на SQL-файл в `.py`. Содержимое миграции от номера не зависит: 1846 меняет только `v_form4_sheet_jsonb`, 1831 — `add_project`, пересечений по функциям нет. +- Откат: `downgrade()` пустой. Откат — это новая миграция, которая восстанавливает тела из 0050 и 0037 (снять их через `pg_get_functiondef` до применения). +- `ARCHITECTURE.md`: в абзац о `_form_affected_rows` добавить, что FORM_1 и FORM_2 собираются построителями с фокусом (`p_focus_lines`), `sort_order` берётся из скелета листа, FORM_4 — полная сборка до AURORA-1846. + +## Тестирование + +Тесты интеграционные: `api/tests/integration/`, новый файл рядом с `test_upd_form_cells_full_rows.py`. Существующие тесты 1844 остаются и должны проходить. Тестовая БД — PG16 (`perf-pg`, отдельная БД `dfip_test`, см. черновик P3 §0). Контроллер проверяет также на 15.12. Время в тестах не проверяется. + +1. **Побайтная эквивалентность на уровне SQL** (новый хелпер в `sheet_contract.py`). + - Для набора `p_ids` сравниваются `SELECT row_type, depth, sort_order, data::text FROM v3._form_affected_rows(...)` и эталон. + - Эталон — фильтр 0050, применённый к полному `v3.v_form_view` с теми же параметрами: агрегаты по пути плюс INPUT по `line_id`; для сателлитов — по `id`, для AHR_LIMIT — по пути шаблона, для OTCH9F — по разделу. Эталон пишется в коде теста, в БД он не попадает. + - Сравнение `data::text` побайтное: jsonb-текст канонический, поэтому видна и разница в масштабе numeric (`30` и `30.00`). + - Проверка **на каждой строке INPUT** тестового листа по очереди, плюс пакеты из строк разных ветвей. + - Покрытие: FORM_1 AHR (Support и Development), CAP (Support), OPER; FORM_2 AHR, CAP, OPER, AHR_LIMIT, AHR_RENT, AHR_UTILITY, AHR_SECURITY, OTCH9F. Строки OTCH9F вставляются через `db_exec` в `fixed_asset_report` (HTTP-ручки нет), минимум два месяца одного оборудования и второе оборудование той же статьи в том же месяце — так проверяется tie-break. + - Тестовые листы содержат строки в нескольких ветвях дерева, чтобы были первая и последняя строки листа и предки без соседей. +2. **Эквивалентность через HTTP** (`assert_patch_rows_match_sheet`): PATCH `/cell` и `/cells` на тех же листах, включая очистку (`null`), правку нечисловой колонки и `sections`. +3. **`sort_order` после POST и DELETE line**: добавить несколько строк, удалить строку в середине, править строку ниже неё; ответ сравнить с GET. +4. **Совпадение ключа сортировки**: в откатываемой сессии вставить `budget_line` с явным `id`, равным id дочерней статьи той статьи, к которой привязана строка. Последовательность при этом не трогается. GET дважды даёт один и тот же порядок, узел идёт перед строкой, ответ на правку совпадает с GET. +5. **Дельта АХР**: после правки AHR_RENT (форма 4 фикстуры) результат `SELECT * FROM v3.get_form2_ahr_satellite_delta(4, 'AHR_RENT', id)` (через `db_exec` в той же сессии) совпадает со строками GET AHR того же `sort_order`. В нём есть синхронизированная строка INPUT и предки всех уровней, включая SUB_SUB_ITEM, если они есть на пути. +6. **Пустой фокус**: `get_form2_ahr_satellite_delta` для несуществующего `source_row_id` возвращает 0 строк. Вызов построителя с `'{}'` тоже даёт 0 строк. +7. **Защита правки 0037**: + - GET AHR_LIMIT для строки LEAF «- принтеры» (`limit_template.id = 13`, код `204620030`, засеяна в `0013_form2_limit.sql:1707`) после правки `qty_q1` даёт `amount_q1 = qty_q1 × 0.57 × 10900 / 4`; + - если в справочнике есть `limit_template` с кодом, равным `num_group_id` статьи AHR глубины 4, правка такой строки AHR_LIMIT даёт непустую дельту АХР; если такого шаблона нет, тест делает `skip` с причиной; + - отдельный тест проверяет, что `pg_get_functiondef` новых `v_form2_view` и `get_form2_ahr_satellite_delta` содержит `_form2_ahr_limit_amount` и `ei.num_group_id = lt.expense_item_code` без `ei.depth = 3 AND (`. +8. **Атомарность пакета** — существующий `test_bad_column_in_batch_fails_and_applies_nothing` остаётся. + +## Замер до/после + +Замер выполняет контроллер после реализации, не в ночной прогон. Код стенда `perf/` не меняется. Сценарий — шаг `coauthor` (`perf/e2e/scenarios/scroll-coauthor.spec.ts` и бот `perf/perfkit/coauthor.py`), цель `FORM_2/AHR/*`, rate 1 и 5, масштаб 1, CPU 1x. Метрики берутся из `sql/coauthor/sql_top.json`, `summary.md` и строк `db_time` в `perf/.run/api.log`. + +1. **«До», 1154 строки.** Код ветки 1844, `stand.sh reset`, `perfkit.gen.run --scale 1`, `perfkit.gen.verify --scale 1` без grow, затем coauthor (около 7 мин). Ожидаемое значение — около 310 мс на `upd_form_cell` (одна сборка). +2. **Снимок GET «до».** Для всех листов FORM_1 и FORM_2 из манифеста сохранить JSON ответа `GET /form/{id}/sheet/{sheet}` (с `direction`, где он есть). +3. **Переход на ветку 1850.** `stand.sh migrate`, перезапуск api, снова снимок GET. Ответы должны попарно совпасть (требование «GET листа не меняется»). Различие допустимо только в порядке строк с совпадающим ключом, и такие строки нужно перечислить. +4. **Эквивалентность на данных стенда.** psql, только чтение. Для 50 случайных строк FORM_2/AHR и 20 строк каждого из листов CAP, OPER, AHR_RENT и FORM_1/AHR сравнить `v3._form_affected_rows(form, sheet, ARRAY[id], dir, NULL)` с эталонным фильтром по `v3.v_form_view` (тот же запрос, что в тесте п. 1). Расхождений должно быть 0. +5. **GET без регресса.** В одном psql-соединении сделать 5 прогревочных вызовов, затем 20 раз чередовать `SELECT count(*) FROM v3.v_form_view(, 'AHR', NULL, NULL)` и `SELECT count(*) FROM v3.upd_form_cell(...)` в транзакции с ROLLBACK, измерить по `\timing` или `pg_stat_statements`. Средний GET не больше значения на коде 1844 + 10 % (то же измерение на шаге 1). +6. **«После», 1154 строки.** coauthor, rate 1 и 5. Критерии: + - `mean_exec_time` вызова `upd_form_cell` ≤ 50 мс; + - вызовов `v3.v_form_view` с `toplevel = false` — 0; + - `calls` у `v3.v_form2_view(p_form_id, p_sheet, p_sections)` равно числу GET листа, а не числу правок; + - на rate 5 `(coauthor_echoed + coauthor_errors) / coauthor_sent ≥ 0.95` — очереди нет. +7. **«После», 2092 строки.** `perfkit.gen.grow --scale 1` (около 4 мин), затем coauthor. Критерий: `mean_exec_time` ≤ 50 мс и тот же критерий очереди. + +Отчёт: таблица «до» и «после» по шагам 1, 6, 7 с путями к `sql_top.json` и `summary.md`, результаты шагов 3–5. Общее время — около 45 мин. Точка «до» на 2092 строках не снимается (см. решения без обсуждения). + +## Оставлено исполнителю + +- Конкретная перестройка CTE внутри построителей: разделение `tw`/`tw_focus`, `page`/`page_focus`, размещение скелета. Ранг через `ROW_NUMBER` по скелету или эквивалентный способ — главное, одним выражением для обоих режимов. +- Вычисление узлов фокуса и `∪ desc_ids` в `DECLARE`/`SELECT INTO` перед `RETURN QUERY`, если это даёт лучший план. +- Сужать ли агрегат шаблона AHR_LIMIT. +- Имена тестовых хелперов и раскладка тестов по файлам. +- Текст абзаца в `ARCHITECTURE.md`. + +## Decisions made without discussion + +| Дата | Решение | Обоснование | Цена ошибки | +|---|---|---|---| +| 2026-10-28 | Capability `sheet-cell-edit`. В спецификацию как ADDED вошёл и контракт ответа 1844 (полные строки, явные `null`, `sections`, атомарность пакета) | `openspec/specs/` пуст; без контракта требование о `sort_order` и эквивалентности не к чему привязать | Переименование capability при архивации; пересечение с change 1845 или 1848, если там заведут ту же область | +| 2026-10-28 | Параметр `p_focus_lines integer[] DEFAULT NULL` последним во всех построителях; `NULL` — полный лист, `'{}'` — пустой результат | Совместимость существующих вызовов по позиции; явное различие «весь лист» и «ничего» | Если вызывающая сторона передаст NULL вместо `'{}'`, будет полная сборка, правильная по содержимому, но медленная; ловит тест п. 6 | +| 2026-10-28 | `sort_order` считает построитель по скелету листа в обоих режимах (новая колонка `_sort_rank`); view перестают считать собственный `ROW_NUMBER` | Один источник порядка для GET и правки, требование точного `sort_order` | +1–3 мс к GET на скелет; при ошибке в скелете меняется и GET, что ловит снимок GET на стенде | +| 2026-10-28 | Tie-break `ORDER BY _sort, line_id NULLS FIRST` (узел раньше строки); для OTCH9F — `id` после `(expense_item_id, month)` | Порядок при совпадении ключа сейчас не определён; без tie-break скелет и вывод могут разойтись | Порядок строк в GET меняется только в таких случаях, возможно непривычно для пользователя | +| 2026-10-28 | Новая `v3.v_form1_view`: в неё вынесена ветка FORM_1 AHR/CAP/OPER из `v_form_view` | Одна проекция строки FORM_1 для GET и правки | Ещё одна функция в схеме; при ошибке переноса меняется GET FORM_1, что ловят снимок GET и тесты 1844 | +| 2026-10-28 | Узлы фокуса берутся из `budget_line` без фильтра `direction`, а INPUT — с фильтром | Так ведёт себя 0050: предки по всем строкам правки, INPUT только видимые в листе | Если правильнее было не возвращать предков для строки чужого направления — лишние, но верные строки в ответе | +| 2026-10-28 | Сателлиты: фильтр по `id` и `ROW_NUMBER` по лёгкому скелету; AHR_LIMIT: фильтр вывода по пути шаблона, агрегат шаблона может остаться полным; OTCH9F: `base` по разделам изменённых строк | Размер шаблона AHR_LIMIT не зависит от числа строк формы; окна OTCH9F замкнуты в разделе | AHR_LIMIT остаётся O(размер шаблона) на правку, сейчас это доли мс | +| 2026-10-28 | Дельта АХР теперь включает предков уровня SUB_SUB_ITEM | Контракт «все предки»; после 0033 у AHR есть статьи глубины 4, их агрегат у соавторов устаревал | Если фронт АХР не ожидает таких строк, получит лишние строки; `updateCells` сливает по ключу, так что это безопасно | +| 2026-10-28 | `SET jit = off` на `_form_affected_rows`, `get_form2_ahr_satellite_delta`, `v_form2_sheet_sections` и `v_form1_sheet_sections` (затрагивает и GET) | JIT съедает бюджет 50 мс и через общий кэш планов может начать срабатывать на каждом GET; по М12 JIT на этих запросах только вредит | Если на проде JIT на GET полезен, GET станет медленнее; ловит шаг 5 замера | +| 2026-10-28 | `_form_affected_rows` помечается `STABLE` | Функция только читает, вызывается из STABLE-функции дельты | STABLE-функция видит снимок вызывающего оператора. Сейчас её зовёт отдельный оператор `RETURN QUERY` после `_apply_form_cell`, и правки видны. Если когда-нибудь её вызовут в одном операторе с правкой, ответ будет со старыми значениями; это ловят тесты эквивалентности | +| 2026-10-28 | Тела `v_form2_view` и `get_form2_ahr_satellite_delta` в 0051 — буквальные, снятые через `pg_get_functiondef` с БД после 0050, а не регулярка поверх функции | Правка затрагивает десятки мест; регулярка хрупкая | Если тело снято с БД не той версии, откатится 0037; ловят тесты п. 7 | +| 2026-10-28 | DROP старых сигнатур без CASCADE в одной миграции, затем CREATE в порядке «вызываемая раньше» | Неоднозначные перегрузки с DEFAULT; скрытая зависимость должна ронять миграцию, а не удалять объекты | Падение миграции при неизвестной зависимости — разбор вручную | +| 2026-10-28 | Имя миграции `0051_upd_form_cells_subtree` | Номер из решения человека, имя по change id | Переименование при rebase | +| 2026-10-28 | Эталон эквивалентности — фильтр 0050 поверх полного `v_form_view` в коде теста, сравнение `data::text` побайтно, проход по каждой строке INPUT тестового листа | Требование «побайтно»; dict-сравнение в HTTP не видит масштаб numeric; полный проход закрывает первую и последнюю строку без ручного выбора | Тесты дольше на несколько секунд | +| 2026-10-28 | Порог «GET не медленнее более чем на 10 %» и замер GET в одном соединении с чередованием правок | Нужен численный критерий регресса; чередование проверяет общий кэш планов | Порог 10 % может быть слишком мягким или жёстким при шуме стенда | +| 2026-10-28 | Отсутствие очереди на rate 5 проверяется как `(echoed + errors) / sent ≥ 0.95`, без задержки эха по времени | Бот не пишет задержку эха, а `perf/` сейчас не трогаем; при отсутствии очереди сервер отвечает почти на все отправленные правки | Растущую задержку при почти полном ответе этот критерий не увидит; можно позже добавить в бот p50 и p95 задержки | +| 2026-10-28 | Точка «до» на 2092 строках не снимается; ориентир — 1,46 с из шага memory (две сборки) и около 0,73 с после 1844 по оценке | Критерий абсолютный (≤ 50 мс); экономия около 30 мин на `reset`, `gen` и `grow` с кодом 1844 | В отчёте нет измеренной точки «до» на большом листе, только оценка | +| 2026-10-28 | Снимок GET всех листов FORM_1 и FORM_2 до и после миграции на стенде и SQL-эквивалентность на 50+ строках стенда | Тестовые данные малы; реальный объём и разброс статей есть только на стенде | +10 мин к замеру | diff --git a/openspec/changes/aurora-1850-upd-form-cells-subtree/proposal.md b/openspec/changes/aurora-1850-upd-form-cells-subtree/proposal.md new file mode 100644 index 0000000..59347e4 --- /dev/null +++ b/openspec/changes/aurora-1850-upd-form-cells-subtree/proposal.md @@ -0,0 +1,47 @@ +# Proposal + +## Why + +Правка ячейки на сервере стоит столько же, сколько сборка всего листа. На стенде профилирования (`perf/REPORT.md`, М3; `perf/report-data/20261026-s1/sql/coauthor/sql_top.json`) правка на FORM_2/AHR в 1154 строки занимает 630 мс: `v3.upd_form_cells` дважды собирает лист через `v3.v_form_view`, по 310 мс на сборку. На листе в 2092 строки (после grow) правка занимает 1,46 с. WebSocket-соединение обрабатывает сообщения последовательно, поэтому потолок — около 1,6 правки/с (на 2092 строках около 0,7). Пользователь, который быстро вводит значения, видит, что подтверждения его правок отстают всё сильнее. + +AURORA-1844 (PR #231, база этой ветки) убрала снимок «до» и `_jsonb_diff`. Теперь на правку приходится одна полная сборка. Её вызов вынесен в шов `v3._form_affected_rows`. Но и одна сборка — это около 310 мс, и время по-прежнему растёт с размером листа. Задача AURORA-1850 (Gitea #235, эпик AURORA-1834) убирает и её: правка собирает только затронутое поддерево, то есть изменённые строки и агрегаты их предков. + +Критерий из Jira: правка ячейки на сервере занимает ≤ 50 мс независимо от размера листа, а суммы совпадают с полной сборкой (подтверждается тестом). + +## What Changes + +- `v3._form_affected_rows` для FORM_1 и FORM_2 больше не вызывает `v3.v_form_view` и не собирает лист целиком. Касается листов AHR/CAP/OPER, сателлитов AHR_RENT/AHR_UTILITY/AHR_SECURITY, а также AHR_LIMIT и OTCH9F. +- В существующие построители листа (`v3.v_form1_sheet_sections`, `v3.v_form2_sheet_sections` и их вызывающие) добавляется параметр «фокус по строкам». Значение NULL означает прежнее поведение, то есть полный лист. Параллельной копии сборки нет: GET листа и ответ на правку считаются одним кодом. +- `sort_order` в ответе на правку остаётся глобальным: он совпадает с номером строки в полном GET того же листа с теми же параметрами. +- `v3.get_form2_ahr_satellite_delta` переводится на тот же механизм. Это фоновое обновление листа АХР после правки или добавления строки сателлита, и сейчас оно строит весь AHR. Правка миграции 0037 (сопоставление строк AHR_LIMIT со статьями глубины 4) сохраняется. +- FORM_4 остаётся на полной сборке, пока не слита AURORA-1846 (PR #232). Критерий производительности для FORM_4 переносится. +- Новая миграция `0051_upd_form_cells_subtree`, номер — в стеке после 0050 из AURORA-1844. Номер может измениться при rebase (см. design.md). +- Новые интеграционные тесты эквивалентности сравнивают каждую строку ответа на правку с полным GET. + +**Не меняется:** +- контракт HTTP (`PATCH /form/{id}/sheet/{sheet}/cell(s)`) и WebSocket (`cell_updated`): форма строк, состав строк, правила `sections`; +- содержимое GET листа. Исключение — детерминированный порядок строк при совпадении ключа сортировки (design.md, D6); +- проверки прав (`v3.upd_form_cells` с `p_user_id`, `can_edit`, `computed_field`), синхронизация сателлитов, `v3._apply_form_cell`; +- `add_budget_line` и `del_budget_line`: они по-прежнему отдают строки через полный `v_form_view`. Это задел на потом, не в этой задаче; +- фронт. + +## Capabilities + +### New Capabilities +- `sheet-cell-edit`: ответ на правку ячеек листа формы (HTTP и WebSocket), то есть состав строк, их содержимое и `sort_order` относительно полного GET; фоновое обновление АХР после правки сателлита; время правки на сервере. Каталог `openspec/specs/` пока пуст, поэтому в спецификацию вместе с новыми требованиями попадает и контракт, установленный AURORA-1844. + +### Modified Capabilities +Нет. + +## Impact + +- SQL (`api/alembic/versions/`): новая миграция `0051_*.py` и `sql/0051_*.sql`. Пересоздаются `v3.v_form2_sheet_sections`, `v3.v_form2_sheet_jsonb`, `v3.v_form2_view`, `v3.v_form1_sheet_sections` и `v3.v_form1_sheet_jsonb`: у них меняется сигнатура, поэтому нужен DROP и CREATE. Появляется новая `v3.v_form1_view`. Переписываются `v3.v_form_view` (ветка FORM_1), `v3._form_affected_rows` и `v3.get_form2_ahr_satellite_delta`. Тело `v_form2_view` и `get_form2_ahr_satellite_delta` берётся из БД после миграции 0037. +- Python (`api/src`) не меняется: сигнатуры `upd_form_cell(s)` и `get_form2_ahr_satellite_delta` прежние. +- Тесты: `api/tests/integration/` — эквивалентность на базе `sheet_contract.py`. +- `ARCHITECTURE.md`: абзац о `_form_affected_rows` дополняется описанием фокусной сборки. +- Связанные задачи: + - AURORA-1844 — шов и контракт полных строк, обязательная предпосылка. + - AURORA-1846 — FORM_4. После её слияния — отдельная задача на фокус FORM_4 по разделу, программе и проекту. + - AURORA-1845 — сужена до кэша пользователя и формы на соединение. Пакетная обработка — отдельная неприоритетная задача. После 1850 ручной ввод не должен создавать очередь, это проверяется замером. + - AURORA-1838 и AURORA-1848 — фронт ищет строку по `(row_type, depth, sort_order)`, поэтому `sort_order` обязан быть точным. Явные `null` в строках ответа нужны для очистки ячеек. + - AURORA-1831 и AURORA-1846 занимают номер миграции 0050 в параллельных PR. diff --git a/openspec/changes/aurora-1850-upd-form-cells-subtree/specs/sheet-cell-edit/spec.md b/openspec/changes/aurora-1850-upd-form-cells-subtree/specs/sheet-cell-edit/spec.md new file mode 100644 index 0000000..11977e3 --- /dev/null +++ b/openspec/changes/aurora-1850-upd-form-cells-subtree/specs/sheet-cell-edit/spec.md @@ -0,0 +1,114 @@ +# Spec Delta + +## Purpose + +Ответ сервера на правку ячеек листа бюджетной формы (HTTP `PATCH /form/{id}/sheet/{sheet}/cell(s)` и WebSocket `cell_updated`): какие строки в нём приходят, какими они должны быть относительно полного GET листа и сколько времени правка может стоить на сервере. + +## ADDED Requirements + +### Requirement: Ответ на правку содержит затронутые строки целиком +Ответ на правку ячеек листов AHR/CAP/OPER форм FORM_1, FORM_2 и FORM_4 SHALL содержать каждую изменённую строку INPUT и все её строки-агрегаты, то есть всех предков по дереву статей до ROOT включительно. Для FORM_4 предками считаются только ROOT раздела, GROUP программы и ITEM проекта изменённой строки. Агрегат SHALL входить в ответ, даже если его значения не изменились. Ответ SHALL NOT содержать строку META и строки других ветвей дерева. Поле `data` каждой строки SHALL быть полной строкой в том же виде, что и в GET листа с теми же `direction` и `sections`: пустые значения передаются явным `null`. Если задан `sections`, ключи верхнего уровня у исключённых разделов SHALL отсутствовать в `data`. + +#### Scenario: Правка числовой ячейки возвращает строку и её предков +- **WHEN** исполнитель меняет `plan.q1` у строки листа FORM_2/AHR +- **THEN** ответ содержит эту строку INPUT и по одной строке-агрегату на каждый уровень её пути в дереве статей +- **AND** `data` каждой строки ответа равно `data` строки того же `sort_order` из последующего GET листа + +#### Scenario: Правка нечисловой ячейки всё равно возвращает предков +- **WHEN** исполнитель меняет `header.justification` у строки FORM_2/AHR +- **THEN** ответ содержит строку INPUT и всех её предков, хотя их суммы не изменились + +#### Scenario: Очистка значения +- **WHEN** исполнитель записывает `null` в `plan.q1` +- **THEN** в строке INPUT ответа ключ `plan.q1` присутствует со значением `null` (или 0 — так же, как в GET) +- **AND** суммы предков в ответе уменьшены на прежнее значение ячейки + +#### Scenario: Правка с фильтром разделов +- **WHEN** правка выполняется с `sections=plan` +- **THEN** ни одна строка ответа не содержит ключей разделов, отличных от `plan`, а также служебных ключей, которых нет в GET с тем же `sections` + +### Requirement: sort_order в ответе совпадает с полным листом +Значения `row_type`, `depth` и `sort_order` каждой строки ответа на правку SHALL совпадать со значениями этой же строки в полном GET листа с теми же параметрами, выполненном сразу после правки. `sort_order` — сквозной номер строки во всём листе, а не номер внутри ответа. Это SHALL выполняться для первой и последней строки листа, после добавления и удаления строк в листе, а также при совпадении ключа сортировки у строки INPUT и узла дерева. Порядок строк в GET при совпадении ключа SHALL быть детерминированным и одинаковым в GET и в ответе на правку. + +#### Scenario: Первая и последняя строка листа +- **WHEN** правится первая по порядку строка INPUT листа, а затем последняя +- **THEN** у каждой строки обоих ответов есть строка с тем же `(row_type, depth, sort_order)` в GET листа, и её `data` совпадает + +#### Scenario: После добавления и удаления строк +- **WHEN** в лист добавлены строки (POST line), одна из строк в середине листа удалена (DELETE line), после чего правится строка, расположенная ниже удалённой +- **THEN** `sort_order` каждой строки ответа равен её `sort_order` в GET листа после этих операций + +#### Scenario: Пакет правок по разным ветвям дерева +- **WHEN** через `PATCH /cells` одним запросом правятся строки двух разных ветвей дерева статей +- **THEN** ответ содержит обе строки INPUT и объединение их предков без дублей, и каждая строка совпадает со строкой GET того же `sort_order` + +#### Scenario: Направление FORM_1 +- **WHEN** правится строка FORM_1/AHR с `direction=Support` +- **THEN** `sort_order` строк ответа равен `sort_order` в GET FORM_1/AHR с `direction=Support` + +### Requirement: Состав ответа на листах сателлитов, AHR_LIMIT и OTCH9F +На листах FORM_2 AHR_RENT, AHR_UTILITY и AHR_SECURITY ответ на правку SHALL содержать только изменённые строки. На AHR_LIMIT ответ SHALL содержать изменённые строки LEAF шаблона и их предков по шаблону (GROUP и SECTION). На OTCH9F ответ SHALL содержать все строки (все месяцы) того же сочетания статьи и оборудования, что и изменённая строка: остатки на начало месяца зависят от предыдущих месяцев. Для всех этих листов SHALL выполняться требование о совпадении `data` и `sort_order` с полным GET. + +#### Scenario: Правка строки аренды +- **WHEN** исполнитель меняет `plan.q1` у строки AHR_RENT +- **THEN** ответ содержит ровно одну строку с `data.id` этой строки, и она совпадает со строкой GET AHR_RENT того же `sort_order` + +#### Scenario: Правка количества в лимитах +- **WHEN** исполнитель меняет `qty_q1` у строки LEAF листа AHR_LIMIT +- **THEN** ответ содержит эту строку и строки её группы и раздела с пересчитанными `amount_q1`, и они совпадают с GET AHR_LIMIT + +#### Scenario: Правка месяца в OTCH9F +- **WHEN** исполнитель меняет поступление за месяц у строки OTCH9F +- **THEN** ответ содержит строки всех месяцев того же оборудования той же статьи с пересчитанными остатками, и они совпадают с GET OTCH9F + +### Requirement: Обновление АХР после правки сателлита +После правки или добавления строки на AHR_RENT, AHR_UTILITY, AHR_SECURITY или AHR_LIMIT формы FORM_2 сервер SHALL отправить подключённым к листу AHR событие (`cell_updated` или `row_added`). Событие SHALL содержать синхронизированные строки AHR и всех их предков, включая уровень SUB_SUB_ITEM. Каждая из этих строк SHALL совпадать со строкой того же `sort_order` полного GET листа AHR. Если синхронизированных строк нет, событие SHALL NOT содержать строк AHR, то есть ответ пуст, а не весь лист. Строки AHR_LIMIT SHALL сопоставляться и со статьями AHR глубины 4 по `num_group_id` (поведение миграции 0037). + +#### Scenario: Правка аренды обновляет АХР +- **WHEN** исполнитель меняет `plan.q1` у строки AHR_RENT, связанной со строкой AHR +- **THEN** строки, которые сервер готовит для события на лист AHR, совпадают со строками GET AHR того же `sort_order`, и среди них есть синхронизированная строка INPUT с новой суммой + +#### Scenario: Источник без синхронизированных строк +- **WHEN** запрашивается обновление АХР для строки сателлита, у которой нет связанной строки AHR +- **THEN** набор строк для события пуст + +#### Scenario: Лимит на статье глубины 4 +- **WHEN** правится строка AHR_LIMIT, код статьи которой совпадает с `num_group_id` статьи AHR глубины 4 +- **THEN** набор строк для события содержит синхронизированную строку AHR этой статьи + +### Requirement: Пакет правок атомарен +Если хотя бы одна правка в пакете отклонена (неизвестная колонка, чужая строка, нет прав, окно закрыто), SHALL NOT применяться ни одна правка пакета, и ответ SHALL быть ошибкой. + +#### Scenario: Ошибочная колонка в пакете +- **WHEN** `PATCH /cells` содержит одну корректную правку и одну с несуществующей колонкой +- **THEN** ответ — ошибка 4xx, и GET листа после запроса совпадает с GET до него + +### Requirement: GET листа не меняется +Содержимое GET листов FORM_1 и FORM_2 (строки, `data`, `sort_order`) после изменения SHALL совпадать с содержимым до него на тех же данных. Исключение — порядок строк с совпадающим ключом сортировки: до изменения он не был определён. Время GET FORM_2/AHR SHALL NOT вырасти больше чем на 10 %. + +#### Scenario: Снимок GET до и после миграции +- **WHEN** на стенде профилирования снят GET всех листов FORM_1 и FORM_2 из манифеста до применения миграции и после неё +- **THEN** ответы попарно совпадают + +#### Scenario: Время GET после смешанной нагрузки +- **WHEN** в одном соединении с БД чередуются правки ячеек и GET листа FORM_2/AHR +- **THEN** среднее время GET не больше базового + 10 % + +### Requirement: Время правки ячейки на FORM_1 и FORM_2 +Правка одной ячейки листа FORM_1 или FORM_2 SHALL NOT собирать лист целиком: на правку SHALL приходиться 0 вложенных полных сборок листа. Среднее серверное время `v3.upd_form_cell` на FORM_2/AHR SHALL быть не больше 50 мс как на листе в 1154 строки, так и на листе в 2092 строки. Требование не распространяется на FORM_4 до слияния AURORA-1846. + +#### Scenario: Соавтор на листе масштаба 1 +- **WHEN** на стенде профилирования (масштаб 1, без grow, FORM_2/AHR 1154 строки) выполняется шаг `coauthor` +- **THEN** в `sql/coauthor/sql_top.json` `mean_exec_time` вызова `v3.upd_form_cell` ≤ 50 мс +- **AND** вызовов `v3.v_form_view` не верхнего уровня (`toplevel = false`) нет, а число вызовов `v3.v_form2_view(p_form_id, p_sheet, p_sections)` равно числу GET листа и не зависит от числа правок + +#### Scenario: Соавтор на выросшем листе +- **WHEN** после `perfkit.gen.grow` (FORM_2/AHR 2092 строки) шаг `coauthor` выполняется повторно +- **THEN** `mean_exec_time` вызова `v3.upd_form_cell` ≤ 50 мс + +### Requirement: Ручной ввод без очереди +Сервер SHALL успевать подтверждать правки одного соединения с частотой ручного ввода (5 правок/с), не накапливая очередь. + +#### Scenario: Соавтор с частотой 5 правок в секунду +- **WHEN** бот соавтора на FORM_2/AHR отправляет 5 правок/с в течение шага `coauthor` +- **THEN** число полученных ответов (эхо и ошибки) составляет не меньше 95 % от числа отправленных правок -- 2.47.2 From dd510d105d7c75a58353d72c98dcac5eec607964 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 20:41:41 +0400 Subject: [PATCH 02/20] =?UTF-8?q?docs(openspec):=20aurora-1850=20=E2=80=94?= =?UTF-8?q?=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=2028.10=20=D0=BF?= =?UTF-8?q?=D0=BE=20JIT=20=D0=B8=20=D1=83=D1=81=D0=BB=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D1=8E=20=D0=BC=D0=B5=D1=80=D0=B6=D0=B0,=20=D1=80=D0=B5=D1=88?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=BB=D0=BB=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../design.md | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md b/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md index fac0162..718be4b 100644 --- a/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md +++ b/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md @@ -60,6 +60,11 @@ 5. Критерий: mean `upd_form_cell` ≤ 50 мс на FORM_2/AHR и 0 вложенных полных сборок листа на правку. Вторая точка снимается после grow (2092 строки). 6. AURORA-1845 сужена до кэша на соединение. После 1850 ручной ввод не должен создавать очередь — это проверяется замером coauthor на rate 5. 7. Номер миграции — 0051, в стеке после 0050 из 1844. При rebase на 1831 и 1846 номер меняется. +8. JIT выключается на уровне наших функций в миграции (`CREATE FUNCTION … SET jit = off`): в построителях `v_form{1,2}_sheet_sections`, в `_form_affected_rows` и в дельте АХР (D7). Конфигурацию сервера Postgres не трогаем — это не зона команды (AURORA-1849). +9. Пороги производительности (≤ 50 мс, GET не больше +10 %, нет очереди на rate 5) — **не условие мержа**. PR вливается по зелёным тестам и доказанной корректности. Цифры замера записываются в PR, недобор оформляется follow-up-задачей. Корректность — **жёсткое условие**, и любое её нарушение означает стоп и эскалацию: + - эквивалентность ответа на правку и полного GET (тесты п. 1–6); + - совпадение снимка GET до и после миграции, кроме строк, порядок которых раньше был недетерминирован из-за совпадения ключа (D6); + - защита правки 0037 (тесты п. 7). ## Decisions @@ -116,17 +121,22 @@ Добавляется tie-break `ORDER BY _sort, line_id NULLS FIRST`: при совпадении узел идёт раньше строки. Для OTCH9F добавляется `id` после `(expense_item_id, month)`. GET меняется только там, где порядок раньше был не определён. -### D7. JIT выключается в построителях и на пути правки +### D7. JIT выключается в построителях и на пути правки (согласовано 28.10, решение 8) `SET jit = off` ставится как атрибут функций `_form_affected_rows`, `get_form2_ahr_satellite_delta`, `v_form2_sheet_sections` и `v_form1_sheet_sections`. Причины две. 1. Запрос с фокусом в custom-плане может оценить стоимость выше `jit_above_cost` и потратить 200+ мс на компиляцию. Это больше всего бюджета в 50 мс. 2. План кэшируется один на оператор, общий для GET и правок в одном backend. Дешёвые custom-планы правок снижают среднюю стоимость custom-плана, и после этого GET может перестать переходить на generic-план. Тогда GET будет каждый раз платить JIT (+210–230 мс, М12). По М12 JIT на этих запросах только вредит: 534 мс против 240 мс. Атрибут `SET` не мешает: plpgsql не инлайнится. +Почему это безопасно на проде: +- `jit` — пользовательский параметр (контекст `PGC_USERSET`). Поэтому `CREATE FUNCTION … SET jit = off` выполняется правами владельца схемы, без суперпользователя. +- Значение действует только во время выполнения функции, включая вложенные вызовы, и восстанавливается при выходе. Настройки сервера, роли и сессии не меняются, другие запросы приложения JIT по-прежнему используют по общей конфигурации. +- Связь с AURORA-1849 («Прод-Postgres: согласовать work_mem и jit»): та задача о серверной или ролевой конфигурации, она остаётся за администраторами. Наш атрибут от её исхода не зависит и с ним не конфликтует. Если на проде JIT уже выключен, атрибут ничего не меняет. `work_mem` в этой задаче не трогаем. + Отвергнутые варианты: -- (а) `jit = off` только на пути правки. Не закрывает причину 2. -- (б) `plan_cache_mode = force_custom_plan`. Добавляет 7–8 мс планирования на каждую правку и усиливает причину 2. -- (в) Глобальная настройка Postgres. Вне репозитория, прод не наш. +- (а) `jit = off` только на пути правки. Не закрывает причину 2. Это запасной ход, если GET регрессирует (см. Risks). +- (б) `plan_cache_mode = force_custom_plan`. Добавляет 7–8 мс планирования на каждую правку и усиливает причину 2. Тоже запасной ход, только на пути правки. +- (в) Настройка сервера Postgres. Не зона команды (решение 8, AURORA-1849). ### D8. Тела функций в миграции — буквальные, снятые с мигрированной БД Тела `v_form2_view` и `get_form2_ahr_satellite_delta` берутся из `pg_get_functiondef` на БД, мигрированной до 0050, и правятся вручную. Регулярные выражения поверх `pg_get_functiondef`, как в 0037, не используются. @@ -162,8 +172,15 @@ - **Пустой фокус превратится в полный лист** (NULL из `array_agg`). Последствия: дельта АХР отдаст весь лист, правка снова станет дорогой. Защита: семантика `'{}'` (D1), `COALESCE` в вызывающих, тест п. 6. - **Откатится правка 0037** (формула лимитов, сопоставление статей глубины 4). Защита: D8 и тесты п. 7. - **Регресс GET** из-за общего кэша планов, лишнего скелета или выключенного JIT. Защита: D7; требование «GET листа не меняется»; замер GET до и после со смешанной нагрузкой («Замер», шаг 5). -- **Generic-план для запроса с фокусом окажется плохим.** Фильтры фокуса стоят в базовых CTE (`tw`, `page`, `agg`/`aq_q`), которые материализуются, поэтому объём ниже по плану мал при любом плане. Если замер покажет обратное, следующий шаг — `SET plan_cache_mode = force_custom_plan` только на `_form_affected_rows`, с повторным замером. -- **Сумма ROOT растёт с размером листа**: агрегат предка суммирует все строки под ним. Числовой `SUM` без jsonb на 2092 строках, по оценке, укладывается в 10–20 мс, а вложенные циклы `tw ⋈ aq_q` (около 100 мс) сужаются до 4–5 узлов. Если вторая точка (2092 строки) превысит 50 мс, исполнитель останавливается и эскалирует с профилем EXPLAIN: инкрементальные суммы — это уже другая задача. +- **Generic-план для запроса с фокусом окажется плохим.** Фильтры фокуса стоят в базовых CTE (`tw`, `page`, `agg`/`aq_q`), которые материализуются, поэтому объём ниже по плану мал при любом плане. +- **Сумма ROOT растёт с размером листа**: агрегат предка суммирует все строки под ним. Числовой `SUM` без jsonb на 2092 строках, по оценке, укладывается в 10–20 мс, а вложенные циклы `tw ⋈ aq_q` (около 100 мс) сужаются до 4–5 узлов. Инкрементальные суммы — это уже другая задача. +- **Регресс производительности** (GET больше +10 %, правка больше 50 мс, очередь на rate 5) мерж не блокирует (решение 9). Исполнитель может итерировать **только на пути правки**, по шагам: + 1. `SET plan_cache_mode = force_custom_plan` на `_form_affected_rows` и дельте; + 2. снять `SET jit = off` с построителей, оставив его на `_form_affected_rows` и дельте; + 3. если не помогло — стоп и вопрос контроллеру с EXPLAIN (ANALYZE, BUFFERS) запроса правки и GET. + + Скелет `sort_order` (D2) без контроллера не откатывать. Оставшийся недобор фиксируется в PR и уходит в follow-up. +- **Нарушение корректности** (расхождение эквивалентности, снимка GET или защиты 0037) — стоп и эскалация, никаких обходов (решение 9). - **Права, аудит, синхронизация сателлитов** не затронуты: это слой выше `_form_affected_rows`. - **Зависимости.** AURORA-1844 — шов и контракт, это база ветки. AURORA-1846 нужна только для будущего фокуса FORM_4. Номер миграции может конфликтовать с 1831 и 1846. @@ -176,7 +193,13 @@ ## Тестирование -Тесты интеграционные: `api/tests/integration/`, новый файл рядом с `test_upd_form_cells_full_rows.py`. Существующие тесты 1844 остаются и должны проходить. Тестовая БД — PG16 (`perf-pg`, отдельная БД `dfip_test`, см. черновик P3 §0). Контроллер проверяет также на 15.12. Время в тестах не проверяется. +Тесты интеграционные: `api/tests/integration/`, новый файл рядом с `test_upd_form_cells_full_rows.py`. Существующие тесты 1844 остаются и должны проходить. Время в тестах не проверяется. + +Тестовое окружение: +- Полный `pytest` гоняется на PG16: контейнер `perf-pg`, порт 55432, отдельная БД `dfip_1850`, создаётся командой `docker exec perf-pg createdb -U perf dfip_1850`. Запуск: `DATABASE_URL=postgresql+asyncpg://perf:perf@localhost:55432/dfip_1850`; `--init_db` только на пустой БД. Пароли `OPENBAO__TEST_*` по умолчанию `admin123` (`api/tests/conftest.py`). +- **БД `perf` (стенд замеров) не трогать никогда.** Накатить туда 0051 раньше замера «до» нельзя. +- Тела функций для D8 снимаются через `pg_get_functiondef` с тестовой БД `dfip_1850` на ревизии 0050, до создания 0051. +- Совместимость с PostgreSQL 15.12 проверяет контроллер (контейнер `dfip-pg15`, порт 55434). В плане это отдельный шаг. 1. **Побайтная эквивалентность на уровне SQL** (новый хелпер в `sheet_contract.py`). - Для набора `p_ids` сравниваются `SELECT row_type, depth, sort_order, data::text FROM v3._form_affected_rows(...)` и эталон. @@ -190,15 +213,14 @@ 4. **Совпадение ключа сортировки**: в откатываемой сессии вставить `budget_line` с явным `id`, равным id дочерней статьи той статьи, к которой привязана строка. Последовательность при этом не трогается. GET дважды даёт один и тот же порядок, узел идёт перед строкой, ответ на правку совпадает с GET. 5. **Дельта АХР**: после правки AHR_RENT (форма 4 фикстуры) результат `SELECT * FROM v3.get_form2_ahr_satellite_delta(4, 'AHR_RENT', id)` (через `db_exec` в той же сессии) совпадает со строками GET AHR того же `sort_order`. В нём есть синхронизированная строка INPUT и предки всех уровней, включая SUB_SUB_ITEM, если они есть на пути. 6. **Пустой фокус**: `get_form2_ahr_satellite_delta` для несуществующего `source_row_id` возвращает 0 строк. Вызов построителя с `'{}'` тоже даёт 0 строк. -7. **Защита правки 0037**: - - GET AHR_LIMIT для строки LEAF «- принтеры» (`limit_template.id = 13`, код `204620030`, засеяна в `0013_form2_limit.sql:1707`) после правки `qty_q1` даёт `amount_q1 = qty_q1 × 0.57 × 10900 / 4`; - - если в справочнике есть `limit_template` с кодом, равным `num_group_id` статьи AHR глубины 4, правка такой строки AHR_LIMIT даёт непустую дельту АХР; если такого шаблона нет, тест делает `skip` с причиной; - - отдельный тест проверяет, что `pg_get_functiondef` новых `v_form2_view` и `get_form2_ahr_satellite_delta` содержит `_form2_ahr_limit_amount` и `ei.num_group_id = lt.expense_item_code` без `ei.depth = 3 AND (`. +7. **Защита правки 0037** — по одному тесту на каждую функцию: + - **`v_form2_view` (формула лимитов).** GET AHR_LIMIT для строки LEAF «- принтеры» (`limit_template.id = 13`, код `204620030`, засеяна в `0013_form2_limit.sql:1707`) после правки `qty_q1` даёт `amount_q1 = qty_q1 × 0.57 × 10900 / 4`. Тот же тест проверяет, что `pg_get_functiondef` новой `v_form2_view` содержит `_form2_ahr_limit_amount`. + - **`get_form2_ahr_satellite_delta` (сопоставление по `num_group_id`).** Правка строки AHR_LIMIT, код шаблона которой равен `num_group_id` статьи AHR глубины 4, даёт непустую дельту АХР с синхронизированной строкой этой статьи. Если такого шаблона в справочнике нет, тест делает `skip` с причиной. Тот же тест проверяет, что `pg_get_functiondef` новой дельты содержит `ei.num_group_id = lt.expense_item_code` вне условия `ei.depth = 3 AND (`. 8. **Атомарность пакета** — существующий `test_bad_column_in_batch_fails_and_applies_nothing` остаётся. ## Замер до/после -Замер выполняет контроллер после реализации, не в ночной прогон. Код стенда `perf/` не меняется. Сценарий — шаг `coauthor` (`perf/e2e/scenarios/scroll-coauthor.spec.ts` и бот `perf/perfkit/coauthor.py`), цель `FORM_2/AHR/*`, rate 1 и 5, масштаб 1, CPU 1x. Метрики берутся из `sql/coauthor/sql_top.json`, `summary.md` и строк `db_time` в `perf/.run/api.log`. +Замер выполняет контроллер после основных ночных замеров. В плане это точка ожидания: PR открывается без цифр, цифры дописываются в PR позже. Пороги — не условие мержа (решение 9). Шаги 3 и 4 проверяют корректность: расхождение в них — стоп и эскалация. Код стенда `perf/` не меняется. Сценарий — шаг `coauthor` (`perf/e2e/scenarios/scroll-coauthor.spec.ts` и бот `perf/perfkit/coauthor.py`), цель `FORM_2/AHR/*`, rate 1 и 5, масштаб 1, CPU 1x. Метрики берутся из `sql/coauthor/sql_top.json`, `summary.md` и строк `db_time` в `perf/.run/api.log`. 1. **«До», 1154 строки.** Код ветки 1844, `stand.sh reset`, `perfkit.gen.run --scale 1`, `perfkit.gen.verify --scale 1` без grow, затем coauthor (около 7 мин). Ожидаемое значение — около 310 мс на `upd_form_cell` (одна сборка). 2. **Снимок GET «до».** Для всех листов FORM_1 и FORM_2 из манифеста сохранить JSON ответа `GET /form/{id}/sheet/{sheet}` (с `direction`, где он есть). @@ -234,7 +256,6 @@ | 2026-10-28 | Узлы фокуса берутся из `budget_line` без фильтра `direction`, а INPUT — с фильтром | Так ведёт себя 0050: предки по всем строкам правки, INPUT только видимые в листе | Если правильнее было не возвращать предков для строки чужого направления — лишние, но верные строки в ответе | | 2026-10-28 | Сателлиты: фильтр по `id` и `ROW_NUMBER` по лёгкому скелету; AHR_LIMIT: фильтр вывода по пути шаблона, агрегат шаблона может остаться полным; OTCH9F: `base` по разделам изменённых строк | Размер шаблона AHR_LIMIT не зависит от числа строк формы; окна OTCH9F замкнуты в разделе | AHR_LIMIT остаётся O(размер шаблона) на правку, сейчас это доли мс | | 2026-10-28 | Дельта АХР теперь включает предков уровня SUB_SUB_ITEM | Контракт «все предки»; после 0033 у AHR есть статьи глубины 4, их агрегат у соавторов устаревал | Если фронт АХР не ожидает таких строк, получит лишние строки; `updateCells` сливает по ключу, так что это безопасно | -| 2026-10-28 | `SET jit = off` на `_form_affected_rows`, `get_form2_ahr_satellite_delta`, `v_form2_sheet_sections` и `v_form1_sheet_sections` (затрагивает и GET) | JIT съедает бюджет 50 мс и через общий кэш планов может начать срабатывать на каждом GET; по М12 JIT на этих запросах только вредит | Если на проде JIT на GET полезен, GET станет медленнее; ловит шаг 5 замера | | 2026-10-28 | `_form_affected_rows` помечается `STABLE` | Функция только читает, вызывается из STABLE-функции дельты | STABLE-функция видит снимок вызывающего оператора. Сейчас её зовёт отдельный оператор `RETURN QUERY` после `_apply_form_cell`, и правки видны. Если когда-нибудь её вызовут в одном операторе с правкой, ответ будет со старыми значениями; это ловят тесты эквивалентности | | 2026-10-28 | Тела `v_form2_view` и `get_form2_ahr_satellite_delta` в 0051 — буквальные, снятые через `pg_get_functiondef` с БД после 0050, а не регулярка поверх функции | Правка затрагивает десятки мест; регулярка хрупкая | Если тело снято с БД не той версии, откатится 0037; ловят тесты п. 7 | | 2026-10-28 | DROP старых сигнатур без CASCADE в одной миграции, затем CREATE в порядке «вызываемая раньше» | Неоднозначные перегрузки с DEFAULT; скрытая зависимость должна ронять миграцию, а не удалять объекты | Падение миграции при неизвестной зависимости — разбор вручную | @@ -244,3 +265,7 @@ | 2026-10-28 | Отсутствие очереди на rate 5 проверяется как `(echoed + errors) / sent ≥ 0.95`, без задержки эха по времени | Бот не пишет задержку эха, а `perf/` сейчас не трогаем; при отсутствии очереди сервер отвечает почти на все отправленные правки | Растущую задержку при почти полном ответе этот критерий не увидит; можно позже добавить в бот p50 и p95 задержки | | 2026-10-28 | Точка «до» на 2092 строках не снимается; ориентир — 1,46 с из шага memory (две сборки) и около 0,73 с после 1844 по оценке | Критерий абсолютный (≤ 50 мс); экономия около 30 мин на `reset`, `gen` и `grow` с кодом 1844 | В отчёте нет измеренной точки «до» на большом листе, только оценка | | 2026-10-28 | Снимок GET всех листов FORM_1 и FORM_2 до и после миграции на стенде и SQL-эквивалентность на 50+ строках стенда | Тестовые данные малы; реальный объём и разброс статей есть только на стенде | +10 мин к замеру | +| 2026-10-28 | Контроллер: при регрессе производительности (GET > +10 %, правка > 50 мс, очередь на rate 5) исполнитель итерирует только на пути правки: `plan_cache_mode = force_custom_plan` на `_form_affected_rows`/дельте; снятие `SET jit = off` с построителей при сохранении его на пути правки; если не помогает — стоп и вопрос контроллеру с EXPLAIN. Скелет `sort_order` (D2) без контроллера не откатывается | Пороги не блокируют мерж (решение 9), но итерации не должны задевать GET и корректность; D2 — основа точного `sort_order` | Лишние итерации исполнителя; недобор цифр уходит в follow-up вместо доработки в этом PR | +| 2026-10-28 | Контроллер: полный pytest на PG16 в контейнере `perf-pg` (порт 55432), отдельная БД `dfip_1850`; БД `perf` стенда не трогается никогда; тела функций для D8 снимаются через `pg_get_functiondef` с `dfip_1850` на ревизии 0050; совместимость с PG 15.12 проверяет контроллер (`dfip-pg15`, порт 55434) отдельным шагом плана | Изоляция от стенда замеров: 0051 в БД `perf` до замера «до» испортит базовую точку | При ошибке в БД — пересоздать `dfip_1850`; если 0051 попадёт в `perf`, придётся заново делать reset и gen стенда (около 15 мин) | +| 2026-10-28 | Контроллер: замеры на стенде выполняет контроллер после основных ночных замеров; в плане — точка ожидания, PR открывается без цифр | Стенд занят ночным прогоном; пороги не условие мержа | Цифры в PR появляются позже мержа, если ревью пройдёт раньше | +| 2026-10-28 | Контроллер: тест защиты 0037 разнесён по функциям — формула лимитов через `v_form2_view`, сопоставление `num_group_id` через дельту | Каждая из двух правок 0037 живёт в своей функции; падение теста сразу указывает, где откат | Нет: только раскладка тестов | -- 2.47.2 From 5264fdbf971a0a16aee1cfee20b5b3857d21809e Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 21:04:13 +0400 Subject: [PATCH 03/20] =?UTF-8?q?docs(plan):=20aurora-1850=20=E2=80=94=20?= =?UTF-8?q?=D0=BF=D0=BB=D0=B0=D0=BD=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit План в 10 задач по design.md: тестовая БД dfip_1850 на 0050 и тела функций до 0051 (D8), красный тест эквивалентности, построители с фокусом и _sort_rank (FORM_2, затем FORM_1 и v_form1_view), _form_affected_rows, дельта АХР, jit = off, защита 0037, документация. Решения плана дописаны в design.md. Co-Authored-By: Claude Opus 5.5 --- ...0-28-aurora-1850-upd-form-cells-subtree.md | 799 ++++++++++++++++++ .../design.md | 9 + 2 files changed, 808 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-28-aurora-1850-upd-form-cells-subtree.md diff --git a/docs/superpowers/plans/2026-10-28-aurora-1850-upd-form-cells-subtree.md b/docs/superpowers/plans/2026-10-28-aurora-1850-upd-form-cells-subtree.md new file mode 100644 index 0000000..1e882a7 --- /dev/null +++ b/docs/superpowers/plans/2026-10-28-aurora-1850-upd-form-cells-subtree.md @@ -0,0 +1,799 @@ +# AURORA-1850: правка ячейки пересчитывает только затронутое поддерево. План реализации + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Ветка:** `feat/235-aurora-1850-upd-form-cells-subtree` (Gitea #235, Jira AURORA-1850, эпик AURORA-1834). +**База стека:** `feat/215-aurora-1844-upd-form-cells-full-rows` (PR #231, миграция 0050). PR этой ветки идёт в базу стека, не в `test`. +**Worktree:** `/Users/andrey.epifancev/projects/fomichev_claude/DFiP_Budget_planing/.claude/worktrees/aurora-1850` + +**Goal:** `v3._form_affected_rows` для FORM_1 и FORM_2 перестаёт собирать лист целиком. Ответ на правку строят те же построители, что и GET, но с фокусом на изменённые строки и их предков. `sort_order` при этом остаётся глобальным. + +**Architecture:** Построители `v_form{1,2}_sheet_sections` → `v_form{1,2}_sheet_jsonb` → `v_form2_view` / новая `v_form1_view` получают последним параметром `p_focus_lines integer[] DEFAULT NULL`. NULL — полный лист, `'{}'` — пустой результат. `sort_order` считается в построителе по «скелету» листа (`_sort_rank`) одинаково в обоих режимах, с tie-break D6. `_form_affected_rows` и дельта АХР вызывают построители с фокусом. Всё делается одной миграцией `0051_upd_form_cells_subtree` (DROP старых сигнатур без CASCADE, затем CREATE). + +**Tech Stack:** PostgreSQL (PL/pgSQL, LANGUAGE sql), совместимость с 15.12; Alembic (`run_sql_file_migration`); pytest + FastAPI `TestClient` (интеграционные тесты на PG16, контейнер `perf-pg`). + +**Spec:** `openspec/changes/aurora-1850-upd-form-cells-subtree/` (`proposal.md`, `design.md`, `specs/sheet-cell-edit/spec.md`). Решения D1–D9, Risks, «Тестирование» и «Замер до/после» в `design.md` обязательны. Ссылки вида «D4», «п. 7» ниже ведут туда. + +## Global Constraints + +**Окружение (правила волны 2, обязательны для всех исполнителей и ревьюеров):** +- Переменные, которыми пользуется план: + - `W=/Users/andrey.epifancev/projects/fomichev_claude/DFiP_Budget_planing/.claude/worktrees/aurora-1850` + - `A=$W/.superpowers/aurora-1850` — локальные артефакты, каталог в `.gitignore`; + - `PY=/Users/andrey.epifancev/projects/fomichev_claude/.venvs/dfip-api/bin` — свой venv не создавать; + - `DBURL=postgresql+asyncpg://perf:perf@localhost:55432/dfip_1850`. +- **Замок ночного прогона.** Перед КАЖДЫМ тяжёлым шагом (pytest, alembic, docker, psql в контейнере) выполнить `$A/wait_lock.sh`: он ждёт, пока существует `/private/tmp/claude-501/-Users-andrey-epifancev-projects-fomichev-claude/6fa9c75e-eb11-4e5d-98c1-05eb590ab14f/scratchpad/BENCH_LOCK`. Ожидание запускать с большим timeout или в фоне с уведомлением; если инструмент не даёт ждать долго — повторять проверку. Чтение кода, правки файлов и git-коммиты при замке разрешены. Контроллер передаёт это правило в бриф каждого сабагента. +- БД: только `dfip_1850`, `dfip_1850_base` и `dfip_1850_snap` в контейнере `perf-pg` (порт 55432). **БД `perf` не трогать никогда.** Стенд `perf/` и порты 8000/8080/8081/8082 не запускать. Замеры делает главный контроллер. +- Полный pytest: `cd $W/api && DATABASE_URL=$DBURL $PY/pytest -q -p no:cacheprovider -W ignore`. Исходно падают ровно 3 чужих теста: `test_backend_calls_form3_add_project_and_add_line_functions` и два в `test_export_service_query_params.py`. Новых падений быть не должно. +- PG 15.12 проверяет главный контроллер (контейнер `dfip-pg15`, порт 55434), исполнитель туда не ходит. SQL пишется без возможностей PG16: без `any_value`, у каждого подзапроса в FROM есть алиас. +- Карта кода — `mcp__codegraph__codegraph_explore` с `projectPath=$W`. Если ответ пуст, один раз отметить это в ledger и дальше пользоваться Read/Grep. +- `.bounded` не создавать: правки в `api/alembic` разрешает этот план. + +**Код (из design.md):** +- Миграция: `api/alembic/versions/0051_upd_form_cells_subtree.py` (`revision = "0051"`, `down_revision = "0050"`, `downgrade()` пустой) и `api/alembic/versions/sql/0051_upd_form_cells_subtree.sql`. +- `api/src`, `web/`, `perf/` не меняются. Сигнатуры `v_form_view`, `_form_affected_rows`, `upd_form_cell(s)` и `get_form2_ahr_satellite_delta` прежние. +- `p_focus_lines integer[] DEFAULT NULL` — последний параметр у `v_form2_sheet_sections`, `v_form2_sheet_jsonb`, `v_form2_view`, `v_form1_sheet_sections`, `v_form1_sheet_jsonb` и `v_form1_view`. `NULL` — полный лист, `'{}'` — пустой результат. Вызывающие передают `COALESCE(..., '{}')`. +- Узлы фокуса: `_expense_item_path_set(eid строк фокуса)` ∩ `tw`. eid берутся из `budget_line` без фильтра `direction`. INPUT фокуса — строки фокуса, прошедшие обычный фильтр `page`. +- `_sort_rank bigint` — новая последняя выходная колонка `*_sheet_sections` и `*_sheet_jsonb`. Это `ROW_NUMBER() OVER (ORDER BY _sort, line_id NULLS FIRST)` по скелету: все узлы `tw` плюс все строки `page ⋈ tw`, без jsonb и сумм. Скелет одинаков в обоих режимах. View берут `sort_order` из `_sort_rank`. Скелет (D2) без контроллера не откатывать. +- OTCH9F: ранг `ROW_NUMBER() OVER (ORDER BY expense_item_id, month, id)`. +- Порядок в 0051 (D9): сначала все `DROP FUNCTION` без CASCADE и без `IF EXISTS` — `v_form2_view`, `v_form2_sheet_jsonb`, `v_form2_sheet_sections`, `v_form1_sheet_jsonb`, `v_form1_sheet_sections`. Затем `CREATE` по порядку: `*_sheet_sections`, `*_sheet_jsonb`, `v_form2_view`, `v_form1_view`, `v_form_view` (`CREATE OR REPLACE`), `_form_affected_rows` (`CREATE OR REPLACE`), `get_form2_ahr_satellite_delta` (`CREATE OR REPLACE`). +- Формат SQL-файла (`api/alembic/sql_migration.py`): строки, начинающиеся с `--`, выбрасываются. `AS $function$` и закрывающий `$function$` стоят на разных строках, закрывающий — в начале строки. После каждого оператора идёт `;`. +- Тела функций — буквальные, взятые из `pg_get_functiondef` с `dfip_1850` на ревизии 0050 (файл `before-0051.sql`, Task 1) и правленные вручную. `regexp_replace` поверх `pg_get_functiondef` не используется (D8). +- `SET jit = off` ставится только на `v_form2_sheet_sections`, `v_form1_sheet_sections`, `_form_affected_rows` и `get_form2_ahr_satellite_delta`. Конфигурацию сервера и `work_mem` не трогать. +- `_form_affected_rows` объявляется `STABLE`. Ветка FORM_4 и листы, которые не редактируются (SMETA), остаются на полной сборке. + +**Стоп и итерации (design.md, Risks; решение 9):** +- **Нарушение корректности — стоп и эскалация контроллеру, без обходов.** Сюда относятся: расхождение эквивалентности (ответ на правку ≠ полный GET), расхождение снимка GET до и после миграции (кроме строк с совпадающим ключом, D6), падение защиты 0037, падение миграции на PG15. +- **Регресс производительности** (GET больше +10 %, правка больше 50 мс, очередь на rate 5) мерж не блокирует. Итерировать можно **только на пути правки** и строго по шагам: + 1. `SET plan_cache_mode = force_custom_plan` на `_form_affected_rows` и дельте; + 2. снять `SET jit = off` с построителей, оставив его на `_form_affected_rows` и дельте; + 3. если не помогло — стоп и вопрос контроллеру с `EXPLAIN (ANALYZE, BUFFERS)` запроса правки и GET. + + Недобор записывается в PR и уходит в follow-up. + +**Коммиты, PR, маркировка:** +- Стиль `perf(sql): …`, `test(sheet): …`, `docs(...): …`, по-русски, `git add` только конкретных файлов. Последняя строка — `Co-Authored-By: Claude Opus 5.5 `. Не мержить, не пушить в `test`/`main`, `git stash` без тега не использовать. +- Решение, принятое без обсуждения, дописывается строкой в `openspec/changes/aurora-1850-upd-form-cells-subtree/design.md` → `## Decisions made without discussion`. +- Маркеры: `T.` — minor-находки ревью задачи n; `C-n`/`I-n`/`M-n` — финальное ревью; `R-n` — рулинги контроллера (рядом номер строки в design.md); `O-n` — наблюдения; `H-n` — замечания человека в PR. Статус каждой находки — `fixed `, `deferred` или `rejected <причина>`. Таблица находок ведётся в ledger, раздел `## Findings`. + +## Review Focus + +1. **Пустой фокус превращается в полный лист.** `array_agg` по пустому набору даёт NULL, и вызов с `p_focus_lines = NULL` вернёт весь лист. Ожидание: `'{}'` даёт 0 строк у каждого построителя, у `_form_affected_rows` и у дельты. Тест `test_empty_focus_returns_nothing` (Task 2). +2. **Строка FORM_1 чужого направления в пакете.** Правка строки Development при `direction=Support` возвращает предков (eid без фильтра направления), но не саму строку. Тест: случай FORM_1/AHR/Support в `test_affected_rows_match_reference` включает в пакет строку Development (Task 2). +3. **OTCH9F с `equipment_name IS NULL`.** Разделы окна по NULL-оборудованию должны совпасть с полным листом: в фильтре `base` нужен `IS NOT DISTINCT FROM`, а не `=`. Тестовые данные OTCH9F содержат оборудование NULL (Task 2), реализация в Task 4. +4. **Скелет сателлитов теряет или добавляет строки относительно GET.** В GET есть `JOIN v3.budget_line` и `JOIN v3.vsp`. Если скелет нумерует по `rent_detail` без того же соединения, `sort_order` сдвинется. Ожидание: в скелете тот же набор строк, что в GET. Тест: эквивалентность сателлитов по каждой строке (Task 2), реализация в Task 4. +5. **Фокус при `sections`.** С `p_sections` построитель в режиме фокуса должен давать те же байты, что полный лист с тем же `sections`, а `_form_affected_rows` должен вырезать ключи исключённых разделов. Тест: случай `sections=['plan']` в `test_affected_rows_match_reference` (Task 2) и существующий `test_sections_filter_drops_other_sections_keys`. + +--- + +## Карта файлов + +| Файл | Что | Задачи | +|---|---|---| +| `$A/wait_lock.sh`, `$A/reset_db.sh`, `$A/snap.sh`, `$A/snap_seed.sql`, `$A/snap_query.sql` | локальные скрипты: замок, сброс тестовой БД, снимок GET; не коммитятся | 1 | +| `openspec/changes/aurora-1850-upd-form-cells-subtree/before-0051.sql` | тела 8 функций с ревизии 0050 (`pg_get_functiondef`): исходник для D8, база для ревью и отката | 1 | +| `api/tests/integration/sheet_contract.py` | SQL-хелперы эквивалентности: эталон 0050 поверх `v_form_view`, фактический ответ, сравнение | 2 | +| `api/tests/integration/test_upd_form_cells_subtree.py` | тесты эквивалентности, отсутствия полной сборки, пустого фокуса, `sort_order`, дельты АХР, JIT | 2, 8 | +| `api/tests/integration/test_upd_form_cells_subtree_0037_guard.py` | защита правки 0037 | 9 | +| `api/alembic/versions/0051_upd_form_cells_subtree.py` | обёртка миграции | 3 | +| `api/alembic/versions/sql/0051_upd_form_cells_subtree.sql` | все функции изменения | 3–8 | +| `ARCHITECTURE.md` | абзац о `_form_affected_rows` | 10 | +| `openspec/changes/aurora-1850-upd-form-cells-subtree/design.md` | решения без обсуждения (дописываются по ходу) | все | + +## Модели исполнителей + +| Задача | Модель | Почему | +|---|---|---| +| 1. Подготовка БД, тела функций, снимок GET | sonnet | окружение и SQL-посев по описанию | +| 2. Красные тесты эквивалентности | sonnet | подбор данных фикстуры, интеграция с API | +| 3. FORM_2 AHR/CAP/OPER: фокус и `_sort_rank` | sonnet | SQL-интеграция, 700-строчный построитель | +| 4. FORM_2: сателлиты, AHR_LIMIT, OTCH9F | sonnet | SQL-интеграция | +| 5. FORM_1: фокус, `_sort_rank`, `v_form1_view` | sonnet | SQL-интеграция | +| 6. `_form_affected_rows` | sonnet | SQL-интеграция | +| 7. Дельта АХР | haiku | тело задано планом и дампом | +| 8. JIT на функциях | haiku | готовый тест и атрибуты | +| 9. Защита 0037 | haiku | готовые тесты | +| 10. ARCHITECTURE, прогон с нуля, снимок GET | haiku | команды и текст заданы | +| Ревью задач | sonnet | — | +| Финальное ревью всей ветки | **opus** | — | + +--- + +### Task 1: Тестовая БД на ревизии 0050, тела функций, снимок GET «до» + +Шаг идёт **до** создания 0051 (D8, «Тестирование»). + +**Files:** +- Create (не коммитятся): `$A/wait_lock.sh`, `$A/reset_db.sh`, `$A/snap.sh`, `$A/snap_seed.sql`, `$A/snap_query.sql`, `$A/get-before.tsv` +- Create: `openspec/changes/aurora-1850-upd-form-cells-subtree/before-0051.sql` + +**Interfaces:** +- Produces: + - БД `dfip_1850_base` (ревизия 0050 + `tests/sqls/fixture.sql`) — шаблон для сброса; + - `$A/reset_db.sh` — пересоздаёт `dfip_1850` из шаблона и выполняет `alembic upgrade head`; + - `$A/snap.sh before|after` — снимок GET на засеянной `dfip_1850_snap`; + - `before-0051.sql` — исходные тела функций. + +- [ ] **Step 1: Скрипты замка и сброса** + +`$A/wait_lock.sh`: +```bash +#!/usr/bin/env bash +L=/private/tmp/claude-501/-Users-andrey-epifancev-projects-fomichev-claude/6fa9c75e-eb11-4e5d-98c1-05eb590ab14f/scratchpad/BENCH_LOCK +while [ -e "$L" ]; do sleep 60; done +``` +`$A/reset_db.sh`: +```bash +#!/usr/bin/env bash +set -euo pipefail +A="$(cd "$(dirname "$0")" && pwd)"; W="$(cd "$A/../.." && pwd)" +PY=/Users/andrey.epifancev/projects/fomichev_claude/.venvs/dfip-api/bin +"$A/wait_lock.sh" +docker exec perf-pg dropdb -U perf --if-exists dfip_1850 +docker exec perf-pg createdb -U perf -T dfip_1850_base dfip_1850 +cd "$W/api" && DATABASE_URL=postgresql+asyncpg://perf:perf@localhost:55432/dfip_1850 "$PY/alembic" upgrade head +``` +`chmod +x` на оба. + +- [ ] **Step 2: Базовый прогон на 0050 и шаблон** + +Run: +```bash +$A/wait_lock.sh && docker exec perf-pg dropdb -U perf --if-exists dfip_1850 && docker exec perf-pg createdb -U perf dfip_1850 \ + && cd $W/api && DATABASE_URL=$DBURL $PY/pytest --init_db -q -p no:cacheprovider -W ignore +``` +Expected: падают ровно 3 исходных теста, тесты `test_upd_form_cells_full_rows.py` проходят. Итоговую строку записать в ledger. + +Затем создать шаблон: +```bash +docker exec perf-pg dropdb -U perf --if-exists dfip_1850_base && docker exec perf-pg createdb -U perf -T dfip_1850 dfip_1850_base +``` + +- [ ] **Step 3: Снять тела функций** + +```bash +$A/wait_lock.sh && docker exec perf-pg psql -U perf -d dfip_1850 -At -c " +SELECT string_agg(pg_get_functiondef(CAST(s AS regprocedure)) || ';', E'\n\n' ORDER BY o) + FROM unnest(ARRAY[ + 'v3.v_form2_sheet_sections(integer,character varying,text[])', + 'v3.v_form2_sheet_jsonb(integer,character varying,text[])', + 'v3.v_form2_view(integer,character varying,text[])', + 'v3.v_form1_sheet_sections(integer,character varying,character varying,text[])', + 'v3.v_form1_sheet_jsonb(integer,character varying,character varying,text[])', + 'v3.v_form_view(integer,character varying,text[],character varying,integer)', + 'v3._form_affected_rows(integer,character varying,integer[],character varying,text[])', + 'v3.get_form2_ahr_satellite_delta(integer,character varying,integer)' + ]) WITH ORDINALITY AS t(s, o)" > $W/openspec/changes/aurora-1850-upd-form-cells-subtree/before-0051.sql +``` +Первой строкой файла дописать комментарий: `-- pg_get_functiondef с dfip_1850, ревизия 0050 (AURORA-1850, D8). Не миграция: исходник для 0051, база ревью и отката.` + +Проверить, что дамп снят после 0037: +```bash +grep -c "_form2_ahr_limit_amount" before-0051.sql # ≥ 4 (v_form2_view, AHR_LIMIT) +grep -n "ei.num_group_id = lt.expense_item_code" before-0051.sql # есть в get_form2_ahr_satellite_delta +``` +Если хотя бы одна проверка не проходит — стоп и эскалация: значит, БД не на 0050. + +- [ ] **Step 4: Найти вызывающих построители** + +```bash +docker exec perf-pg psql -U perf -d dfip_1850 -At -c " +SELECT p.oid::regprocedure FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace + WHERE n.nspname = 'v3' AND p.prosrc ~ '(v_form[12]_sheet_(sections|jsonb)|v_form2_view)\s*\('" +``` +Записать список в ledger. Для каждой функции вне списка из 8 проверить по `prosrc`, что вызов идёт по позиции не больше чем с прежним числом аргументов и не через `SELECT *` в запись фиксированной формы. Иначе новая колонка `_sort_rank` её сломает. Ожидается, например, `v3.v_form1_smeta`: она выбирает колонки по имени, это безопасно. Если найдена опасная — стоп и вопрос контроллеру. + +- [ ] **Step 5: Снимок GET «до» на засеянных данных** + +`$A/snap_seed.sql` — один `DO`-блок, детерминированный (строки вставляются `ORDER BY`): +- создать формы `v3.add_budget_form('FORM_1', 2050, 1)` и `v3.add_budget_form('FORM_2', 2050, 1)`; +- на листах AHR/CAP/OPER каждой формы вставить `budget_line` на каждую третью листовую статью (`row_number() OVER (PARTITION BY form, sheet ORDER BY ei.id) % 3 = 1`), привязанную к типу формы через `expense_item_form_type`. `direction` = `ei.direction` для FORM_1 и NULL для FORM_2. Обязательные колонки `budget_line` добавить по схеме таблицы; +- по каждой паре (форма, лист, направление) выполнить `PERFORM * FROM v3.upd_form_cells(form, sheet, , direction)`, а для половины строк ещё и `plan.q3`; +- вставить в `v3.fixed_asset_report` формы FORM_2: две статьи, которые попадают в ветку OTCH9F `v_form2_view` (`JOIN v3.expense_item`); у первой — оборудование `'E1'` за месяцы 1–3, `'E2'` за месяц 1 и `NULL` за месяцы 1–2; у второй — `'E1'` за месяц 1. Заполнить `opening_*`, `acquired_*` и `disposed_*`; +- `INSERT INTO v3.form_limit (budget_form_id, template_id, qty_q1)` — для трёх LEAF шаблона, включая `id = 13`. + +`$A/snap_query.sql`: +```sql +WITH s(ft, sheet, dir) AS (VALUES + ('FORM_1','AHR','Support'),('FORM_1','AHR','Development'),('FORM_1','CAP','Support'), + ('FORM_1','CAP','Development'),('FORM_1','OPER',NULL),('FORM_1','SMETA',NULL), + ('FORM_2','AHR',NULL),('FORM_2','CAP',NULL),('FORM_2','OPER',NULL),('FORM_2','AHR_LIMIT',NULL), + ('FORM_2','AHR_RENT',NULL),('FORM_2','AHR_UTILITY',NULL),('FORM_2','AHR_SECURITY',NULL), + ('FORM_2','OTCH9F',NULL),('FORM_2','SMETA',NULL)) +SELECT bf.id, s.sheet, COALESCE(s.dir, '-'), v.sort_order, v.row_type, v.depth, v.data::text + FROM v3.budget_form bf + JOIN s ON s.ft = bf.form_type_code + CROSS JOIN LATERAL v3.v_form_view(bf.id, s.sheet, NULL, s.dir) v + ORDER BY 1, 2, 3, 4; +``` +`$A/snap.sh`: +```bash +#!/usr/bin/env bash +set -euo pipefail +A="$(cd "$(dirname "$0")" && pwd)"; W="$(cd "$A/../.." && pwd)" +PY=/Users/andrey.epifancev/projects/fomichev_claude/.venvs/dfip-api/bin +PSQL="docker exec -i perf-pg psql -U perf -v ON_ERROR_STOP=1 -q" +"$A/wait_lock.sh" +docker exec perf-pg dropdb -U perf --if-exists dfip_1850_snap +docker exec perf-pg createdb -U perf -T dfip_1850_base dfip_1850_snap +$PSQL -d dfip_1850_snap < "$A/snap_seed.sql" +if [ "${1:?before|after}" = after ]; then + (cd "$W/api" && DATABASE_URL=postgresql+asyncpg://perf:perf@localhost:55432/dfip_1850_snap "$PY/alembic" upgrade head) +fi +$PSQL -d dfip_1850_snap -At -F $'\t' < "$A/snap_query.sql" > "$A/get-$1.tsv" +wc -l "$A/get-$1.tsv" +``` +Run: `$A/snap.sh before`, затем `$A/snap.sh before` ещё раз, сохранив первый результат под другим именем, и `diff` двух файлов. +Expected: `get-before.tsv` содержит не меньше 500 строк, повторный прогон даёт идентичный файл (посев детерминирован). + +- [ ] **Step 6: Коммит** + +```bash +git add openspec/changes/aurora-1850-upd-form-cells-subtree/before-0051.sql +git commit -m "docs(openspec): aurora-1850 — тела функций до 0051 (pg_get_functiondef, ревизия 0050) + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 2: Красные тесты эквивалентности + +**Files:** +- Modify: `api/tests/integration/sheet_contract.py` (добавить SQL-хелперы; `row_key` и `assert_patch_rows_match_sheet` не трогать) +- Create: `api/tests/integration/test_upd_form_cells_subtree.py` + +**Interfaces:** +- Consumes: + - фикстуры `headers` и `db_exec(sql: str, **params) -> list[Row]`; + - хелперы `_new_form`, `_add_line`, `_get_sheet`, `_patch_cell`, `_qs` из `test_upd_form_cells_full_rows.py` — импортировать оттуда, не копировать. +- Produces (в `sheet_contract.py`): + - `Row4 = tuple[str, int, int, str]` — `(row_type, depth, sort_order, data::text)`; + - `strip_keys(sections: list[str] | None) -> list[str]` — тот же список из 22 ключей, что `v_strip` в 0050; + - `sheet_kind(sheet: str) -> str` — `'sat'` для AHR_RENT/AHR_UTILITY/AHR_SECURITY, `'limit'`, `'otch9f'`, иначе `'tree'`; + - `reference_affected_rows(db_exec, form_id, sheet, ids, direction=None, sections=None, strip=True) -> list[Row4]`; + - `affected_rows(db_exec, form_id, sheet, ids, direction=None, sections=None) -> list[Row4]`; + - `focus_builder_rows(db_exec, form_type, form_id, sheet, ids, direction=None, sections=None) -> list[Row4]`; + - `assert_rows_equal(actual: list[Row4], expected: list[Row4], label: str) -> None` — при расхождении сообщение называет первую отличающуюся строку и длины списков. + +- [ ] **Step 1: Хелперы в `sheet_contract.py`** + +Эталон — фильтр 0050 поверх полного `v_form_view`. Это оракул: запрос задаётся буквально, параметры приводятся явно (asyncpg). +```python +_REF_FROM = """ + SELECT v.row_type, v.depth, v.sort_order, (v.data - CAST(:strip AS text[]))::text + FROM v3.v_form_view(:f, :s, CAST(:sec AS text[]), CAST(:d AS varchar)) v + WHERE {where} + ORDER BY v.sort_order""" +_REF_WHERE = { + "sat": "v.row_type = 'INPUT' AND (v.data->>'id')::int = ANY(CAST(:ids AS int[]))", + "limit": "(v.data->>'id')::int = ANY(v3._limit_template_path_set(CAST(:ids AS int[])))", + "otch9f": """EXISTS (SELECT 1 FROM v3.fixed_asset_report far + WHERE far.id = ANY(CAST(:ids AS int[])) + AND far.expense_item_id = (v.data->>'expense_item_id')::int + AND far.equipment_name IS NOT DISTINCT FROM (v.data->>'equipment_name'))""", + "tree": """(v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM','SUB_SUB_ITEM') + AND (v.data->'header'->>'expense_item_id')::int = ANY(v3._expense_item_path_set( + ARRAY(SELECT DISTINCT bl.expense_item_id FROM v3.budget_line bl + WHERE bl.id = ANY(CAST(:ids AS int[])))))) + OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::int = ANY(CAST(:ids AS int[])))""", +} +``` +- `affected_rows` — `SELECT r.row_type, r.depth, r.sort_order, r.data::text FROM v3._form_affected_rows(:f, :s, CAST(:ids AS int[]), CAST(:d AS varchar), CAST(:sec AS text[])) r ORDER BY r.sort_order`. +- `focus_builder_rows`: + - для FORM_2 — `v3.v_form2_view(:f, :s, CAST(:sec AS text[]), CAST(:ids AS int[]))`; + - для FORM_1 — `v3.v_form1_view(:f, :s, CAST(:sec AS text[]), CAST(:d AS varchar), CAST(:ids AS int[]))`; + - результат `ORDER BY sort_order`; сравнивается с `reference_affected_rows(..., strip=False)`. + +- [ ] **Step 2: Тесты в `test_upd_form_cells_subtree.py`** + +Годы новых форм — 2041–2060, у каждого теста свой. Тесты: + +1. `test_affected_rows_match_reference[path, case]` — `path ∈ {"affected", "builder"}`. Случаи `case`: + - FORM_1: AHR/Support, AHR/Development, CAP/Support, OPER/`None`; + - FORM_2: AHR, CAP, OPER, AHR_RENT, AHR_UTILITY, AHR_SECURITY, AHR_LIMIT, OTCH9F; + - отдельно FORM_2/AHR с `sections=['plan']`. + + Данные листов с деревом: + - новая форма; + - строки на первой и последней листовой статье по `mv_expense_item_tree.path` среди привязанных к типу формы; + - две строки-сестры с общим родителем; + - строка в другом ROOT; + - у всех строк `plan.q1` (через PATCH), у одной ещё `header.justification`; + - для FORM_1/AHR/Support дополнительно строка с `direction=Development` (Review Focus 2). + + Данные остальных листов: + - сателлиты: форма 4, по 3 строки через `POST .../line` (`vsp_id`, `contract_number`; обязательные поля UTILITY и SECURITY взять из ветки `v3.add_budget_line`), PATCH `plan.q1`; + - AHR_LIMIT: форма 4, PATCH `qty_q1` у LEAF 13 и ещё у двух LEAF; + - OTCH9F: форма 4, `INSERT` через `db_exec` в `v3.fixed_asset_report` — та же раскладка, что в `snap_seed.sql` (Task 1), включая `equipment_name IS NULL` (Review Focus 3). + + Проверки: для **каждой** строки листа по отдельности (id INPUT-строк из GET; для AHR_LIMIT — все LEAF, для сателлитов — все `data.id`) `assert_rows_equal(actual, reference, label=f"{case} id={i}")`. Для `path="affected"` фактический результат — `affected_rows`, для `path="builder"` — `focus_builder_rows` при `strip=False`. Дополнительно проверяется пакет `[первая, последняя, сестра, строка другого ROOT]` (для FORM_1/AHR/Support — плюс строка Development) и пакет с повтором id. +2. `test_edit_path_does_not_call_v_form_view[case]` — случаи FORM_1/AHR/Support, FORM_2/AHR, AHR_RENT, AHR_LIMIT, OTCH9F и `delta`. + - Если `SELECT rolsuper FROM pg_roles WHERE rolname = current_user` ложно — `pytest.skip("track_functions требует суперпользователя")`. + - Иначе: `SET LOCAL track_functions = 'all'`, затем `n0 = COALESCE(pg_stat_get_xact_function_calls(CAST('v3.v_form_view(integer,character varying,text[],character varying,integer)' AS regprocedure)), 0)`. + - Вызвать `_form_affected_rows` (для `delta` — `get_form2_ahr_satellite_delta(4, 'AHR_RENT', )`). + - `assert n1 - n0 == 0`. +3. `test_empty_focus_returns_nothing` — 0 строк у каждого из вызовов: + - `v3.v_form2_view(4, s, NULL, '{}')` для s ∈ {AHR, CAP, OPER, AHR_RENT, AHR_UTILITY, AHR_SECURITY, AHR_LIMIT, OTCH9F}; + - `v3.v_form1_view(1, 'AHR', NULL, 'Support', '{}')`; + - `v3._form_affected_rows(4, 'AHR', '{}')`; + - `v3.get_form2_ahr_satellite_delta(4, 'AHR_RENT', -1)`. +4. `test_patch_cells_two_branches_matches_get` — `PATCH /cells` по строкам двух ROOT формы FORM_2/AHR. `assert_patch_rows_match_sheet`; в ответе обе INPUT; ключи `row_key` без дублей. +5. `test_patch_after_add_and_delete_matches_get[FORM_2/AHR, FORM_1/AHR/Support]` — добавить 5 строк, удалить третью сверху (`DELETE .../line/{id}`), сделать PATCH строки ниже неё. `assert_patch_rows_match_sheet(patch, GET)`; у каждой строки ответа `sort_order` совпадает с `sort_order` строки GET с тем же `line_id` (для INPUT) или `header.expense_item_id` (для узлов). +6. `test_patch_form1_development_matches_get` — PATCH на FORM_1/AHR с `direction=Development`, `assert_patch_rows_match_sheet` с GET того же направления. +7. `test_patch_otch9f_returns_all_months_of_equipment` — PATCH месячного поступления (колонку взять из ветки OTCH9F `_apply_form_cell`) у строки `'E1'` месяца 1. Ответ = все строки `(статья, 'E1')`, остатки пересчитаны, `assert_patch_rows_match_sheet` с GET OTCH9F. +8. `test_sort_key_tie_node_before_line` — новая форма FORM_2. + - Найти статью AHR глубины 3, у которой есть дочерняя статья глубины 4, и обе привязаны к FORM_2; id дочерней не занят в `budget_line`. Если такой нет — `skip` с причиной. + - `INSERT INTO v3.budget_line (id, budget_form_id, expense_item_id, name, …) VALUES (, …)` через `db_exec`, последовательность не трогать. + - Проверки: два GET дают одинаковый порядок; узел дочерней статьи стоит непосредственно перед этой строкой INPUT; PATCH `plan.q1` этой строки совпадает с GET (`assert_patch_rows_match_sheet`). +9. `test_ahr_delta_matches_get_after_rent_edit` — форма 4: POST строки AHR_RENT, PATCH `plan.q1 = 100`. Для `SELECT row_type, depth, sort_order, data::text FROM v3.get_form2_ahr_satellite_delta(4, 'AHR_RENT', :id)` каждая строка равна строке с тем же `sort_order` из `v3.v_form_view(4, 'AHR', NULL, 'Support')` (тоже `data::text`). Среди строк есть INPUT с `plan.q1 = 100`. +10. `test_ahr_delta_includes_sub_sub_item_for_depth4_limit` — форма 4. + - Найти LEAF шаблона, у которого `expense_item_code` равен `num_group_id` статьи AHR/Support глубины 4. Ожидается LEAF 13, код `204620030`. Если такого нет — `skip`. + - PATCH `qty_q1 = 4` этого LEAF на AHR_LIMIT (синхронизация создаёт строку AHR). + - Дельта `(4, 'AHR_LIMIT', )` непуста и построчно совпадает с GET AHR, как в п. 9. В ней есть строка `SUB_SUB_ITEM` с `header.expense_item_id` статьи глубины 4. + +- [ ] **Step 3: Убедиться, что тесты красные по правильной причине** + +Run: `$A/wait_lock.sh && cd $W/api && DATABASE_URL=$DBURL $PY/pytest tests/integration/test_upd_form_cells_subtree.py -q -p no:cacheprovider -W ignore` +Expected на 0050: +- **FAIL**: все `path=builder` (`function v3.v_form2_view(integer, …, integer[]) does not exist` или `v3.v_form1_view … does not exist`); `test_edit_path_does_not_call_v_form_view` (`n1 - n0 == 1`), если он не skip; `test_empty_focus_returns_nothing` (нет сигнатуры); `test_ahr_delta_includes_sub_sub_item_for_depth4_limit` (нет SUB_SUB_ITEM). +- **PASS** (характеризация 0050, подтверждает эталон): все `path=affected`, тесты 4–7 и 9. +- Тест 8 до D6 может пройти или упасть случайно, это нормально. + +Если какой-то `path=affected` падает, ошибка в эталоне или в данных: исправить тест, а не трогать SQL. + +- [ ] **Step 4: Коммит** + +```bash +git add api/tests/integration/sheet_contract.py api/tests/integration/test_upd_form_cells_subtree.py +git commit -m "test(sheet): эквивалентность ответа на правку и полной сборки — красные тесты (AURORA-1850) + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 3: Миграция 0051 и FORM_2 AHR/CAP/OPER — фокус и `_sort_rank` + +**Files:** +- Create: `api/alembic/versions/0051_upd_form_cells_subtree.py` +- Create: `api/alembic/versions/sql/0051_upd_form_cells_subtree.sql` + +**Interfaces:** +- Consumes: тела `v_form2_sheet_sections`, `v_form2_sheet_jsonb` и `v_form2_view` из `before-0051.sql`. +- Produces: + - `v3.v_form2_sheet_sections(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL, p_focus_lines integer[] DEFAULT NULL)` — `RETURNS TABLE(<прежние колонки до _sort_path включительно>, _sort_rank bigint)`, plpgsql, STABLE; + - `v3.v_form2_sheet_jsonb(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL, p_focus_lines integer[] DEFAULT NULL)` — `RETURNS TABLE(<прежние>, _sort_rank bigint)`, LANGUAGE sql, проекция с передачей `p_focus_lines`; + - `v3.v_form2_view(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL, p_focus_lines integer[] DEFAULT NULL)` — `RETURNS TABLE(row_type varchar, depth integer, sort_order bigint, data jsonb)`. Ветка AHR/CAP/OPER: `sort_order = j._sort_rank`, `ORDER BY j._sort_rank`. Остальные ветки в этой задаче — без изменений, тело из дампа. + +- [ ] **Step 1: Обёртка миграции** + +```python +from sql_migration import run_sql_file_migration + + +revision = "0051" +down_revision = "0050" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + run_sql_file_migration("0051_upd_form_cells_subtree.sql") + + +def downgrade() -> None: + pass +``` + +- [ ] **Step 2: SQL — DROP и три функции FORM_2** + +Файл начинается с комментария со ссылкой на AURORA-1850 и change. Затем: +```sql +DROP FUNCTION v3.v_form2_view(integer, character varying, text[]); +DROP FUNCTION v3.v_form2_sheet_jsonb(integer, character varying, text[]); +DROP FUNCTION v3.v_form2_sheet_sections(integer, character varying, text[]); +``` +(DROP FORM_1 добавит Task 5 сразу после этих строк.) Затем `CREATE FUNCTION` в порядке sections → jsonb → view. Тела копируются из дампа и правятся так (D1, D2, D6): +- в DECLARE считать `v_focus_nodes int[]` и `v_focus_eids int[]` (объединение `desc_ids` узлов фокуса из `v3.mv_expense_item_tree`), только если `p_focus_lines IS NOT NULL`. При пустом фокусе — `'{}'`, а не NULL; +- `tw` и `page` остаются полными: из них строится скелет. Рядом появляются отфильтрованные `tw_f` (`p_focus_lines IS NULL OR id = ANY(v_focus_nodes)`) и `page_f` (`p_focus_lines IS NULL OR lid = ANY(p_focus_lines)`); +- `jp..jq4`, `input_base` и `final_input` строятся по `page_f`; +- `agg` и `aq_q` ограничиваются `p_focus_lines IS NULL OR eid = ANY(v_focus_eids)`; +- `tw_agg` и `tw_aq1..4` строятся по `tw_f`; +- скелет `sk` — узлы `tw` (`_sort = path`, `line_id = NULL`) ∪ строки `page ⋈ tw` (`_sort = tree_path || lid`, `line_id = lid`), ранг `ROW_NUMBER() OVER (ORDER BY _sort, line_id NULLS FIRST)`. Вывод соединяется со скелетом по ключу узла или строки; +- итоговый `ORDER BY` — по `_sort_rank`. + +Конкретная раскладка CTE остаётся за исполнителем (design.md, «Оставлено исполнителю»). Единственное ограничение: никакой ветки `IF p_focus_lines IS NULL THEN <копия запроса>` (D1в). + +- [ ] **Step 3: Применить и прогнать тесты FORM_2 AHR/CAP/OPER** + +Run: +```bash +$A/reset_db.sh && cd $W/api && DATABASE_URL=$DBURL $PY/pytest tests/integration/test_upd_form_cells_subtree.py tests/integration/test_upd_form_cells_full_rows.py -q -p no:cacheprovider -W ignore -rf +``` +Expected: +- PASS: `test_affected_rows_match_reference` для FORM_2 AHR/CAP/OPER в обоих `path` и с `sections=['plan']`; все `path=affected`; весь `test_upd_form_cells_full_rows.py`. +- Ещё красные (их закроют Task 4–7), список `-rf` должен состоять только из них: `path=builder` для сателлитов, LIMIT, OTCH9F и FORM_1; `test_empty_focus_returns_nothing`; тест «без полной сборки»; дельта SUB_SUB_ITEM; JIT ещё не написан. + +- [ ] **Step 4: Снимок GET** + +Run: `$A/snap.sh after && diff $A/get-before.tsv $A/get-after.tsv` +Expected: пустой diff. Допустимы только перестановки строк с совпадающим ключом (D6). Каждую такую перестановку перечислить в отчёте задачи с обоснованием. Любое другое расхождение — **стоп и эскалация**. + +- [ ] **Step 5: Коммит** + +```bash +git add api/alembic/versions/0051_upd_form_cells_subtree.py api/alembic/versions/sql/0051_upd_form_cells_subtree.sql +git commit -m "perf(sql): фокус и _sort_rank в построителях FORM_2 AHR/CAP/OPER (AURORA-1850) + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 4: FORM_2 — сателлиты, AHR_LIMIT, OTCH9F с фокусом в `v_form2_view` + +**Files:** +- Modify: `api/alembic/versions/sql/0051_upd_form_cells_subtree.sql` (тело `v_form2_view`) + +**Interfaces:** +- Consumes: `v_form2_view(..., p_focus_lines)` из Task 3. +- Produces: та же сигнатура. Для листов AHR_RENT/AHR_UTILITY/AHR_SECURITY `p_focus_lines` — id деталей, для AHR_LIMIT — id `limit_template`, для OTCH9F — id `fixed_asset_report`. + +- [ ] **Step 1: Сателлиты (D4)** + +Ранг — `ROW_NUMBER() OVER (ORDER BY .id)` по скелету без jsonb. **В скелете тот же набор строк, что в GET**: те же `JOIN v3.budget_line` и `JOIN v3.vsp` и тот же `WHERE` (Review Focus 4). Соединение с `vsp` по первичному ключу дешёвое. Убирать его можно только если доказано, что оно не фильтрует строки: `vsp_id NOT NULL` и есть FK. Затем фильтр `p_focus_lines IS NULL OR .id = ANY(p_focus_lines)`, и jsonb строится только по отобранным строкам. + +- [ ] **Step 2: AHR_LIMIT** + +Вывод фильтруется по `p_focus_lines IS NULL OR lt.id = ANY(v3._limit_template_path_set(p_focus_lines))`; `'{}'` даёт NULL и, значит, 0 строк. `sort_order = lt.sort_order`. Агрегат `tree`/`leaf_amt`/`agg` можно оставить полным. Вызовы `v3._form2_ahr_limit_amount(...)` из дампа сохраняются буквально. + +- [ ] **Step 3: OTCH9F** + +`base` ограничивается разделами фокуса: +```sql +AND (p_focus_lines IS NULL OR EXISTS ( + SELECT 1 FROM v3.fixed_asset_report ff + WHERE ff.id = ANY(p_focus_lines) + AND ff.expense_item_id IS NOT DISTINCT FROM far.expense_item_id + AND ff.equipment_name IS NOT DISTINCT FROM far.equipment_name)) +``` +Ранг — отдельный скелет по всем строкам формы с тем же `JOIN v3.expense_item`: `ROW_NUMBER() OVER (ORDER BY expense_item_id, month, id)` (D6). `sort_order` берётся из скелета соединением по `id`. + +- [ ] **Step 4: Тесты** + +Run: `$A/reset_db.sh && cd $W/api && DATABASE_URL=$DBURL $PY/pytest tests/integration/test_upd_form_cells_subtree.py tests/integration/test_upd_form_cells_full_rows.py -q -p no:cacheprovider -W ignore` +Expected: PASS — `test_affected_rows_match_reference` для AHR_RENT, AHR_UTILITY, AHR_SECURITY, AHR_LIMIT и OTCH9F в обоих `path`, а также `test_patch_otch9f_returns_all_months_of_equipment`. Красными остаются только случаи, которые закрывают Task 5–7. + +- [ ] **Step 5: Снимок GET** + +Run: `$A/snap.sh after && diff $A/get-before.tsv $A/get-after.tsv` +Expected: diff пуст. Для OTCH9F допустимы только перестановки при равных `(expense_item_id, month)`, если они есть (D6) — перечислить. Иначе стоп. + +- [ ] **Step 6: Коммит** + +```bash +git add api/alembic/versions/sql/0051_upd_form_cells_subtree.sql +git commit -m "perf(sql): фокус сателлитов, AHR_LIMIT и OTCH9F в v_form2_view (AURORA-1850) + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 5: FORM_1 — фокус, `_sort_rank`, новая `v_form1_view`, `v_form_view` + +**Files:** +- Modify: `api/alembic/versions/sql/0051_upd_form_cells_subtree.sql` + +**Interfaces:** +- Consumes: тела `v_form1_sheet_sections`, `v_form1_sheet_jsonb` и `v_form_view` из `before-0051.sql`. +- Produces: + - `v3.v_form1_sheet_sections(p_form_id integer, p_sheet character varying, p_direction character varying DEFAULT NULL, p_sections text[] DEFAULT NULL, p_focus_lines integer[] DEFAULT NULL)` — `RETURNS TABLE(<прежние>, _sort_rank bigint)`; + - `v3.v_form1_sheet_jsonb(<те же параметры>)` — `RETURNS TABLE(<прежние>, _sort_rank bigint)`, LANGUAGE sql; + - `v3.v_form1_view(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL, p_direction character varying DEFAULT NULL, p_focus_lines integer[] DEFAULT NULL)` — `RETURNS TABLE(row_type varchar, depth integer, sort_order bigint, data jsonb)`, plpgsql, STABLE. Принимает только AHR/CAP/OPER, на остальных листах — `RAISE EXCEPTION 'v_form1_view: unsupported sheet %'`. Тело — `jsonb_build_object` из 16 ключей ветки FORM_1 `v_form_view` в прежнем порядке; `sort_order = j._sort_rank`; + - `v3.v_form_view` — `CREATE OR REPLACE`, сигнатура прежняя. Ветка FORM_1 AHR/CAP/OPER — `RETURN QUERY SELECT * FROM v3.v_form1_view(p_form_id, p_sheet, p_sections, p_direction, NULL); RETURN;`. Остальное без изменений. + +- [ ] **Step 1: DROP FORM_1** + +Сразу после трёх DROP FORM_2: +```sql +DROP FUNCTION v3.v_form1_sheet_jsonb(integer, character varying, character varying, text[]); +DROP FUNCTION v3.v_form1_sheet_sections(integer, character varying, character varying, text[]); +``` + +- [ ] **Step 2: Построители FORM_1** + +Правки те же, что в Task 3, с отличием (контекст design.md): `page` и все агрегатные CTE (`agg`, `aq1..aq4`, `atot`) фильтруются по `direction` так же, как в дампе; `tw` — нет. Узлы фокуса берутся из `budget_line` **без** фильтра `direction`, INPUT фокуса — `page_f` (с фильтром). Скелет — `tw` ∪ `page ⋈ tw` с тем же фильтром направления. + +- [ ] **Step 3: `v_form1_view` и `v_form_view`** + +Порядок `CREATE` по D9: `v_form1_sheet_sections` и `v_form1_sheet_jsonb` вместе с остальными sections и jsonb, затем `v_form2_view`, затем `v_form1_view`, затем `v_form_view`. + +- [ ] **Step 4: Тесты** + +Run: `$A/reset_db.sh && cd $W/api && DATABASE_URL=$DBURL $PY/pytest tests/integration/test_upd_form_cells_subtree.py tests/integration/test_upd_form_cells_full_rows.py -q -p no:cacheprovider -W ignore` +Expected: PASS — все случаи FORM_1 в обоих `path`, включая строку Development в пакете Support; `test_patch_form1_development_matches_get`; `test_patch_after_add_and_delete_matches_get[FORM_1…]`; `test_empty_focus_returns_nothing` (теперь есть обе сигнатуры). Красными остаются только тест «без полной сборки» и тест дельты с SUB_SUB_ITEM. + +- [ ] **Step 5: Снимок GET** + +Run: `$A/snap.sh after && diff $A/get-before.tsv $A/get-after.tsv` → пусто, включая FORM_1 SMETA (`v_form1_smeta` зовёт `v_form1_sheet_sections` с 4 аргументами). Иначе стоп. + +- [ ] **Step 6: Коммит** + +```bash +git add api/alembic/versions/sql/0051_upd_form_cells_subtree.sql +git commit -m "perf(sql): фокус и _sort_rank в построителях FORM_1, новая v_form1_view (AURORA-1850) + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 6: `_form_affected_rows` без полной сборки + +**Files:** +- Modify: `api/alembic/versions/sql/0051_upd_form_cells_subtree.sql` (добавить `CREATE OR REPLACE FUNCTION v3._form_affected_rows` после `v_form_view`) + +**Interfaces:** +- Consumes: `v_form2_view(..., p_focus_lines)`, `v_form1_view(..., p_focus_lines)`; тело 0050 из `before-0051.sql`. +- Produces: `v3._form_affected_rows(p_form_id integer, p_sheet character varying, p_ids integer[], p_direction character varying DEFAULT NULL, p_sections text[] DEFAULT NULL)`, `RETURNS TABLE(row_type varchar, depth integer, sort_order bigint, data jsonb)`, plpgsql, **STABLE**. + +- [ ] **Step 1: Переписать ветки (D5)** + +- `v_strip` — как в 0050. +- FORM_1, листы AHR/CAP/OPER → `SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip FROM v3.v_form1_view(p_form_id, p_sheet, p_sections, p_direction, COALESCE(p_ids, '{}')) v ORDER BY v.sort_order`. +- FORM_2, листы AHR/CAP/OPER/AHR_RENT/AHR_UTILITY/AHR_SECURITY/AHR_LIMIT/OTCH9F → то же через `v3.v_form2_view(p_form_id, p_sheet, p_sections, COALESCE(p_ids, '{}'))`. +- Ветка FORM_4 — буквально как в 0050. +- Остальные сочетания (SMETA и прочие) — прежний путь 0050 через `v_form_view`. + +Фильтров по `_expense_item_path_set` и `_limit_template_path_set` в FORM_1 и FORM_2 больше нет: состав ответа задаёт построитель. + +- [ ] **Step 2: Тесты** + +Run: `$A/reset_db.sh && cd $W/api && DATABASE_URL=$DBURL $PY/pytest tests/integration/test_upd_form_cells_subtree.py tests/integration/test_upd_form_cells_full_rows.py -q -p no:cacheprovider -W ignore` +Expected: +- PASS: все `path=affected`; `test_edit_path_does_not_call_v_form_view` для всех случаев, кроме `delta` (или skip — тогда наблюдение `O-n` в ledger и вопрос контроллеру); все HTTP-тесты; `test_form4_returns_only_own_program_and_project`; `test_bad_column_in_batch_fails_and_applies_nothing`. +- Красными остаются только тесты дельты (п. 2/delta и п. 10). + +- [ ] **Step 3: Коммит** + +```bash +git add api/alembic/versions/sql/0051_upd_form_cells_subtree.sql +git commit -m "perf(sql): _form_affected_rows собирает только затронутое поддерево (AURORA-1850) + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 7: Дельта АХР через `_form_affected_rows` + +**Files:** +- Modify: `api/alembic/versions/sql/0051_upd_form_cells_subtree.sql` (последним оператором файла) + +**Interfaces:** +- Consumes: `_form_affected_rows` из Task 6; тело `get_form2_ahr_satellite_delta` из `before-0051.sql`. +- Produces: `v3.get_form2_ahr_satellite_delta(p_form_id integer, p_source_sheet character varying, p_source_row_id integer)` — сигнатура, `RETURNS TABLE`, `LANGUAGE sql` и `STABLE` прежние. + +- [ ] **Step 1: Тело** + +`CREATE OR REPLACE` с телом из дампа, в котором: +- CTE `target_lines` скопирован **символ в символ**, включая сопоставление `(ei.depth = 3 AND ei.item_id = lt.expense_item_code) OR ei.num_group_id = lt.expense_item_code`; +- CTE `target_path` удалён; +- финальный SELECT заменён на +```sql + SELECT r.row_type, r.depth, r.sort_order, r.data + FROM v3._form_affected_rows( + p_form_id, 'AHR', + COALESCE((SELECT array_agg(tl.line_id) FROM target_lines tl), '{}'), + 'Support', NULL) r + ORDER BY r.sort_order; +``` + +- [ ] **Step 2: Тесты** + +Run: `$A/reset_db.sh && cd $W/api && DATABASE_URL=$DBURL $PY/pytest tests/integration/test_upd_form_cells_subtree.py -q -p no:cacheprovider -W ignore` +Expected: PASS (или skip с указанной причиной) для всего файла, включая `delta`, `test_ahr_delta_matches_get_after_rent_edit`, `test_ahr_delta_includes_sub_sub_item_for_depth4_limit` и пустую дельту в `test_empty_focus_returns_nothing`. + +- [ ] **Step 3: Коммит** + +```bash +git add api/alembic/versions/sql/0051_upd_form_cells_subtree.sql +git commit -m "perf(sql): дельта АХР через _form_affected_rows, с предками SUB_SUB_ITEM (AURORA-1850) + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 8: `SET jit = off` на построителях и пути правки (D7) + +**Files:** +- Modify: `api/tests/integration/test_upd_form_cells_subtree.py` +- Modify: `api/alembic/versions/sql/0051_upd_form_cells_subtree.sql` + +**Interfaces:** +- Consumes: функции из Task 3–7. +- Produces: `proconfig` содержит `jit=off` у `v3.v_form2_sheet_sections`, `v3.v_form1_sheet_sections`, `v3._form_affected_rows` и `v3.get_form2_ahr_satellite_delta`. + +- [ ] **Step 1: Падающий тест** + +```python +JIT_OFF_FUNCS = ("v_form2_sheet_sections", "v_form1_sheet_sections", + "_form_affected_rows", "get_form2_ahr_satellite_delta") + + +def test_jit_off_on_builders_and_edit_path(db_exec): + rows = db_exec( + """SELECT p.proname, COALESCE(p.proconfig, '{}') FROM pg_proc p + JOIN pg_namespace n ON n.oid = p.pronamespace + WHERE n.nspname = 'v3' AND p.proname = ANY(CAST(:names AS text[]))""", + names=list(JIT_OFF_FUNCS)) + assert {r[0] for r in rows} == set(JIT_OFF_FUNCS) + missing = [r[0] for r in rows if "jit=off" not in r[1]] + assert not missing, f"нет SET jit = off: {missing}" +``` + +- [ ] **Step 2: Убедиться, что тест падает** + +Run: `$A/wait_lock.sh && cd $W/api && DATABASE_URL=$DBURL $PY/pytest tests/integration/test_upd_form_cells_subtree.py::test_jit_off_on_builders_and_edit_path -q -p no:cacheprovider -W ignore` +Expected: FAIL, `нет SET jit = off: [...4 имени]`. + +- [ ] **Step 3: Атрибуты** + +В заголовок каждой из 4 функций в 0051 добавить строку ` SET jit TO 'off'` (после `STABLE`, перед `AS $function$`). На другие функции атрибут не ставить. + +- [ ] **Step 4: Тесты** + +Run: `$A/reset_db.sh && cd $W/api && DATABASE_URL=$DBURL $PY/pytest tests/integration/test_upd_form_cells_subtree.py tests/integration/test_upd_form_cells_full_rows.py -q -p no:cacheprovider -W ignore` +Expected: всё PASS (или skip с причиной). + +- [ ] **Step 5: Коммит** + +```bash +git add api/tests/integration/test_upd_form_cells_subtree.py api/alembic/versions/sql/0051_upd_form_cells_subtree.sql +git commit -m "perf(sql): jit = off на построителях и пути правки (AURORA-1850) + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 9: Защита правки 0037 (design.md, «Тестирование», п. 7) + +**Files:** +- Create: `api/tests/integration/test_upd_form_cells_subtree_0037_guard.py` + +**Interfaces:** +- Consumes: фикстуры `headers` и `db_exec`, хелперы `_get_sheet` и `_patch_cell` (импорт из `test_upd_form_cells_full_rows.py`). + +- [ ] **Step 1: Тесты** + +```python +from decimal import Decimal + + +def test_v_form2_view_keeps_0037_limit_formula(client, headers, db_exec): + # LEAF «- принтеры», limit_template.id = 13, код 204620030 (0013_form2_limit.sql:1707) + _patch_cell(client, headers, 4, "AHR_LIMIT", 13, "qty_q1", 4) + row = next(r for r in _get_sheet(client, headers, 4, "AHR_LIMIT") if r["data"]["id"] == 13) + assert Decimal(str(row["data"]["amount_q1"])) == Decimal(4) * Decimal("0.57") * 10900 / 4 + src = db_exec("SELECT pg_get_functiondef(CAST(:s AS regprocedure))", + s="v3.v_form2_view(integer,character varying,text[],integer[])")[0][0] + assert "_form2_ahr_limit_amount" in src + + +OLD_DEPTH3_MATCHER = re.compile(r"ei\.depth\s*=\s*3\s+AND\s*\(") # вид до 0037 + + +def test_ahr_delta_keeps_0037_num_group_matching(client, headers, db_exec): + found = db_exec( + """SELECT lt.id FROM v3.limit_template lt + JOIN v3.expense_item ei ON ei.num_group_id = lt.expense_item_code + AND ei.sheet = 'AHR' AND ei.direction = 'Support' AND ei.depth = 4 + WHERE lt.row_type = 'LEAF' ORDER BY lt.id LIMIT 1""") + if not found: + pytest.skip("нет LEAF шаблона с кодом = num_group_id статьи AHR глубины 4") + lt_id = found[0][0] + _patch_cell(client, headers, 4, "AHR_LIMIT", lt_id, "qty_q1", 4) + delta = db_exec("SELECT row_type FROM v3.get_form2_ahr_satellite_delta(4, 'AHR_LIMIT', :i)", i=lt_id) + assert any(r[0] == "INPUT" for r in delta), "дельта АХР пуста — откат сопоставления 0037" + src = db_exec("SELECT pg_get_functiondef(CAST(:s AS regprocedure))", + s="v3.get_form2_ahr_satellite_delta(integer,character varying,integer)")[0][0] + assert "ei.num_group_id = lt.expense_item_code" in src + assert not OLD_DEPTH3_MATCHER.search(src) +``` +Импорты: `re`, `pytest`, `Decimal`. Сравнение `amount_q1` идёт через `Decimal`, чтобы масштаб numeric не мешал. Если значение в `data` — строка или число, привести через `str`. + +- [ ] **Step 2: Прогон** + +Run: `$A/wait_lock.sh && cd $W/api && DATABASE_URL=$DBURL $PY/pytest tests/integration/test_upd_form_cells_subtree_0037_guard.py -q -p no:cacheprovider -W ignore` +Expected: PASS. Это защитный тест: на 0051 он должен проходить сразу. Если FAIL — откат правки 0037, **стоп и эскалация** (правка тела вручную по дампу, затем снова тест). + +- [ ] **Step 3: Проверка, что тест кусается (без БД)** + +Строковые проверки обоих тестов применить к текстам функций **до** 0037: `get_form2_ahr_satellite_delta` — `api/alembic/versions/sql/0031_form2_ahr_satellite_sync.sql:784-855`, `v_form2_view` — `0033_form2_old_sort.sql:1-373`. Ожидается, что `OLD_DEPTH3_MATCHER.search` находит совпадение в тексте 0031, а `"_form2_ahr_limit_amount"` отсутствует в тексте 0033. Проверить однострочником `$PY/python -c "…"` и записать вывод в ledger. + +- [ ] **Step 4: Коммит** + +```bash +git add api/tests/integration/test_upd_form_cells_subtree_0037_guard.py +git commit -m "test(sheet): защита правки 0037 после 0051 (AURORA-1850) + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 10: ARCHITECTURE.md, прогон с нуля, итоговый снимок GET + +**Files:** +- Modify: `ARCHITECTURE.md` (абзац «Поток редактирования ячейки», строка 5) +- Modify: `openspec/changes/aurora-1850-upd-form-cells-subtree/design.md` (решения без обсуждения, если появились по ходу) + +- [ ] **Step 1: ARCHITECTURE.md** + +Последнее предложение абзаца заменить на: +> Выборку делает `v3._form_affected_rows` (миграции 0050, 0051). Для FORM_1 и FORM_2 она не собирает лист целиком: построители (`v_form1_view`/`v_form2_view` → `*_sheet_jsonb` → `*_sheet_sections`) вызываются с фокусом `p_focus_lines` (NULL — весь лист, `'{}'` — ничего) и считают только изменённые строки и узлы их путей. `sort_order` в обоих режимах берётся из скелета листа (`_sort_rank`), поэтому совпадает с GET. FORM_4 до AURORA-1846 собирается полностью. Фоновая дельта АХР после правки сателлита (`get_form2_ahr_satellite_delta`) идёт тем же путём. + +- [ ] **Step 2: Полный прогон на чистой БД (вся цепочка миграций с нуля)** + +Run: +```bash +$A/wait_lock.sh && docker exec perf-pg dropdb -U perf --if-exists dfip_1850 && docker exec perf-pg createdb -U perf dfip_1850 \ + && cd $W/api && DATABASE_URL=$DBURL $PY/pytest --init_db -q -p no:cacheprovider -W ignore +``` +Expected: падают те же 3 исходных теста, новых падений нет. Число passed = базовое (Task 1) плюс новые тесты. Число skipped и причины skip выписать в ledger. + +- [ ] **Step 3: Итоговый снимок GET** + +Run: `$A/snap.sh after && diff $A/get-before.tsv $A/get-after.tsv` → пусто или только перечисленные перестановки D6. + +- [ ] **Step 4: Ничего лишнего в диффе ветки** + +Run: `git diff --stat origin/feat/215-aurora-1844-upd-form-cells-full-rows...HEAD -- api/src web perf` +Expected: пусто. + +- [ ] **Step 5: Коммит и push** + +```bash +git add ARCHITECTURE.md openspec/changes/aurora-1850-upd-form-cells-subtree/design.md +git commit -m "docs: ARCHITECTURE — фокусная сборка ответа на правку (AURORA-1850) + +Co-Authored-By: Claude Opus 5.5 " +git push origin feat/235-aurora-1850-upd-form-cells-subtree +``` + +--- + +## Завершение (SDD-контроллер) + +1. **Передать главному контроллеру проверку PG15.** Сообщить ветку и коммит и попросить прогнать `pytest --init_db` на `dfip-pg15:55434` (своя БД `dfip_1850`) — миграции с нуля до 0051 и весь набор. Исполнитель на PG15 не ходит. **Ждать ответа.** Падение миграции или новых тестов на PG15 — это нарушение корректности: стоп, правка, повторная передача. +2. **Финальное ревью всей ветки — модель opus** (superpowers:requesting-code-review). Бриф ревьюера: + - дифф `origin/feat/215-aurora-1844-upd-form-cells-full-rows...HEAD`; + - `before-0051.sql` как база для сравнения тел: diff каждой функции «до» и в 0051; + - Review Focus (5 пунктов выше), D1–D9, правило замка. + + Находки `C-n`/`I-n` исправляются в ветке, `M-n` — по решению контроллера. После правок — повтор Task 10, Step 2. +3. **PR без цифр** (superpowers:finishing-a-development-branch): + ```bash + tea pulls create --base feat/215-aurora-1844-upd-form-cells-full-rows \ + --head feat/235-aurora-1850-upd-form-cells-subtree \ + --title "AURORA-1850: правка ячейки пересчитывает только затронутое поддерево" \ + --description "$(cat <файл описания>)" + ``` + Описание PR, по-русски: + - `Closes #235`, Jira AURORA-1850; + - «Что сделано»; + - «Тесты» — итог полного прогона на PG16 с оговоркой про 3 исходных падения и результат PG15 от контроллера; + - «Проверить руками» — правка на FORM_2/AHR и FORM_1/AHR у двух соавторов: сумма предка обновилась у второго, соседние строки не затронуты; правка аренды обновляет АХР; + - «Замер до/после — добавит контроллер после прогона на стенде» и сценарий из design.md, «Замер до/после», шаги 1–7; + - «Rulings I made» — список `R-n` из финального отчёта контроллера; + - таблица находок `| Маркер | Суть | Статус |`, где `deferred` выделены отдельно; + - последняя строка `🤖 Generated with [Claude Code](https://claude.com/claude-code)`. + + Не мержить. +4. **Точка ожидания: замеры главного контроллера** (design.md, «Замер до/после»; после основных ночных замеров). Исполнитель ничего не запускает на стенде и не трогает БД `perf`. Контроллер записывает цифры в PR. + - Шаги 3–4 замера (снимок GET, эквивалентность на данных стенда) разошлись — **стоп и эскалация** (корректность). + - Шаги 5–7 вышли за пороги — итерации только на пути правки, по порядку из Global Constraints («Стоп и итерации»). Каждая итерация — отдельный коммит в этой ветке, полный pytest и повторный замер контроллером. Недобор после шага 3 правила записывается в PR и уходит в follow-up. diff --git a/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md b/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md index 718be4b..c94d432 100644 --- a/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md +++ b/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md @@ -269,3 +269,12 @@ | 2026-10-28 | Контроллер: полный pytest на PG16 в контейнере `perf-pg` (порт 55432), отдельная БД `dfip_1850`; БД `perf` стенда не трогается никогда; тела функций для D8 снимаются через `pg_get_functiondef` с `dfip_1850` на ревизии 0050; совместимость с PG 15.12 проверяет контроллер (`dfip-pg15`, порт 55434) отдельным шагом плана | Изоляция от стенда замеров: 0051 в БД `perf` до замера «до» испортит базовую точку | При ошибке в БД — пересоздать `dfip_1850`; если 0051 попадёт в `perf`, придётся заново делать reset и gen стенда (около 15 мин) | | 2026-10-28 | Контроллер: замеры на стенде выполняет контроллер после основных ночных замеров; в плане — точка ожидания, PR открывается без цифр | Стенд занят ночным прогоном; пороги не условие мержа | Цифры в PR появляются позже мержа, если ревью пройдёт раньше | | 2026-10-28 | Контроллер: тест защиты 0037 разнесён по функциям — формула лимитов через `v_form2_view`, сопоставление `num_group_id` через дельту | Каждая из двух правок 0037 живёт в своей функции; падение теста сразу указывает, где откат | Нет: только раскладка тестов | +| 2026-10-28 | План: шаблон `dfip_1850_base` (ревизия 0050 + фикстура) для быстрого сброса `dfip_1850` и вспомогательная `dfip_1850_snap` для снимка GET; обе в `perf-pg`, БД `perf` не затрагивается | Миграция с DROP без `IF EXISTS` не перезапускается на той же БД; сброс из шаблона быстрее `--init_db` | Две лишние БД в контейнере; при ошибке — пересоздать из `dfip_1850` | +| 2026-10-28 | План: тела 8 функций с ревизии 0050 коммитятся в `openspec/changes/aurora-1850-upd-form-cells-subtree/before-0051.sql` (не миграция) | D8 требует буквальных тел; файл — база для ревью (diff «до» и 0051) и для будущей миграции отката; архивируется вместе с change | Лишний файл ~5 тыс. строк в change | +| 2026-10-28 | План: локальный снимок GET всех листов FORM_1/FORM_2 до и после 0051 на детерминированно засеянной `dfip_1850_snap` после каждой задачи с построителями | Требование «GET листа не меняется» проверяется на стенде только после PR; локальная проверка ловит регресс GET раньше | Данные посева меньше стенда; снимок стенда (шаг 3 замера) остаётся обязательным | +| 2026-10-28 | План: «0 вложенных полных сборок» в тестах проверяется через `SET LOCAL track_functions = 'all'` и `pg_stat_get_xact_function_calls(v_form_view)`; без прав суперпользователя тест делает skip | Единственный способ увидеть вложенный вызов plpgsql-функции из теста | На роли без суперпользователя критерий проверяется только замером стенда (шаг 6) | +| 2026-10-28 | План: тест эквивалентности дополнительно сравнивает с эталоном вывод построителя в режиме фокуса (`v_form2_view`/`v_form1_view` с `p_focus_lines`), а не только `_form_affected_rows` | Даёт красный тест до реализации (путь `_form_affected_rows` на 0050 эквивалентен эталону по построению) и локализует ошибку в построителе | Тесты дольше на несколько секунд | +| 2026-10-28 | План: скелет `sort_order` сателлитов сохраняет те же `JOIN v3.budget_line` и `JOIN v3.vsp`, что и GET; соединение с `vsp` убирается только при доказанных `NOT NULL` и FK | D4 говорит «без соединения с vsp», но в GET соединение внутреннее и может отбрасывать строки; тогда ранг сдвинется | Соединение по первичному ключу почти бесплатно; если убрать его без доказательства — тихо неверный `sort_order` | +| 2026-10-28 | План: `v_form1_view` принимает только AHR/CAP/OPER, на других листах — `RAISE EXCEPTION` | SMETA FORM_1 собирается `v_form_view` до вызова; явная ошибка лучше пустого результата | Нет: функция внутренняя | +| 2026-10-28 | План: раскладка тестов — `test_upd_form_cells_subtree.py` (эквивалентность, `sort_order`, дельта, JIT) и `test_upd_form_cells_subtree_0037_guard.py`; SQL-хелперы в `sheet_contract.py`; HTTP-хелперы и фикстуры импортируются из `test_upd_form_cells_full_rows.py` | Оставлено исполнителю (design.md); решено в плане, чтобы задачи 2, 8, 9 ссылались на одни имена | Нет: только раскладка | +| 2026-10-28 | План: защита 0037 для дельты проверяет `pg_get_functiondef` регуляркой `ei\.depth\s*=\s*3\s+AND\s*\(` (вид до 0037), а не литералом | В теле до 0037 между `3` и `AND (` стоят переводы строки и отступы; литерал не поймал бы откат | Нет | -- 2.47.2 From f9d6a15f076bcdb989899dd602d04c2de2c8733e Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 21:06:19 +0400 Subject: [PATCH 04/20] =?UTF-8?q?docs(openspec):=20aurora-1850=20=E2=80=94?= =?UTF-8?q?=20=D1=80=D1=83=D0=BB=D0=B8=D0=BD=D0=B3=D0=B8=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=82=D1=80=D0=BE=D0=BB=D0=BB=D0=B5=D1=80=D0=B0=20R-1..R?= =?UTF-8?q?-4,=20R-14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../changes/aurora-1850-upd-form-cells-subtree/design.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md b/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md index c94d432..b778b2b 100644 --- a/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md +++ b/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md @@ -278,3 +278,8 @@ | 2026-10-28 | План: `v_form1_view` принимает только AHR/CAP/OPER, на других листах — `RAISE EXCEPTION` | SMETA FORM_1 собирается `v_form_view` до вызова; явная ошибка лучше пустого результата | Нет: функция внутренняя | | 2026-10-28 | План: раскладка тестов — `test_upd_form_cells_subtree.py` (эквивалентность, `sort_order`, дельта, JIT) и `test_upd_form_cells_subtree_0037_guard.py`; SQL-хелперы в `sheet_contract.py`; HTTP-хелперы и фикстуры импортируются из `test_upd_form_cells_full_rows.py` | Оставлено исполнителю (design.md); решено в плане, чтобы задачи 2, 8, 9 ссылались на одни имена | Нет: только раскладка | | 2026-10-28 | План: защита 0037 для дельты проверяет `pg_get_functiondef` регуляркой `ei\.depth\s*=\s*3\s+AND\s*\(` (вид до 0037), а не литералом | В теле до 0037 между `3` и `AND (` стоят переводы строки и отступы; литерал не поймал бы откат | Нет | +| 2026-10-28 | Контроллер (R-1, R-10): скелет сателлитов сохраняет те же JOIN (`budget_line`, `vsp`), что GET (0033); JOIN `vsp` убирать только при доказанных NOT NULL и FK. Одобрено главным контроллером | Рулинг уточняет D4: внутренний JOIN может отбрасывать строки и сдвигать ранг | Тихо неверный `sort_order` сателлитов; ловят тесты эквивалентности и снимок GET | +| 2026-10-28 | Контроллер (R-2): роль `perf` в `perf-pg` и `dfip-pg15` — суперпользователь, `SET LOCAL track_functions` доступен; тест «0 вложенных `v_form_view`» не пропускается. Skip этого теста считается провалом | Иначе критерий «нет полной сборки на пути правки» остаётся непроверенным до замера | Если на другой роли тест скипнется, критерий проверяется только замером стенда | +| 2026-10-28 | Контроллер (R-3): дополнительные БД `dfip_1850_base` и `dfip_1850_snap` в `perf-pg` одобрены; допустим только префикс `dfip_1850*`, БД `perf` не трогается | Быстрый сброс и снимок GET (см. решения плана) | Две лишние БД в контейнере | +| 2026-10-28 | Контроллер (R-4): `before-0051.sql` коммитится в каталог change | База ревью и отката (D8) | Лишний файл в change | +| 2026-10-28 | Контроллер (R-14): подпись коммитов `Co-Authored-By: Claude Opus 5.5` по плану и правилам волны 2, хотя системное напоминание сессии называет Sonnet 5.5 | Явная инструкция пользователя приоритетнее напоминания | Неверное имя модели в трейлере коммитов | -- 2.47.2 From e3b23e467f842faf6aa3e7ea4ce2e8e526bb73ff Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 21:09:56 +0400 Subject: [PATCH 05/20] =?UTF-8?q?docs(openspec):=20aurora-1850=20=E2=80=94?= =?UTF-8?q?=20=D1=82=D0=B5=D0=BB=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D0=B9=20=D0=B4=D0=BE=200051=20(pg=5Fget=5Ffunctiondef,?= =?UTF-8?q?=20=D1=80=D0=B5=D0=B2=D0=B8=D0=B7=D0=B8=D1=8F=200050)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../before-0051.sql | 2629 +++++++++++++++++ 1 file changed, 2629 insertions(+) create mode 100644 openspec/changes/aurora-1850-upd-form-cells-subtree/before-0051.sql diff --git a/openspec/changes/aurora-1850-upd-form-cells-subtree/before-0051.sql b/openspec/changes/aurora-1850-upd-form-cells-subtree/before-0051.sql new file mode 100644 index 0000000..39ad73b --- /dev/null +++ b/openspec/changes/aurora-1850-upd-form-cells-subtree/before-0051.sql @@ -0,0 +1,2629 @@ +-- pg_get_functiondef с dfip_1850, ревизия 0050 (AURORA-1850, D8). Не миграция: исходник для 0051, база ревью и отката. +CREATE OR REPLACE FUNCTION v3.v_form2_sheet_sections(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[]) + RETURNS TABLE(row_type character varying, depth integer, line_id integer, col_section_code character varying, col_item_id character varying, col_num_group_id character varying, col_name character varying, col_vsp_id integer, col_vsp_address character varying, col_plan_q1 numeric, col_plan_q2 numeric, col_plan_q3 numeric, col_plan_q4 numeric, col_plan_year numeric, col_plan_comment character varying, col_seq_dfip_q1 numeric, col_seq_dfip_q2 numeric, col_seq_dfip_q3 numeric, col_seq_dfip_q4 numeric, col_seq_dfip_year numeric, col_seq_dfip_just character varying, col_seq_ssp_q1 numeric, col_seq_ssp_q2 numeric, col_seq_ssp_q3 numeric, col_seq_ssp_q4 numeric, col_seq_ssp_year numeric, col_seq_ssp_just character varying, col_appr_q1 numeric, col_appr_q2 numeric, col_appr_q3 numeric, col_appr_q4 numeric, col_appr_year numeric, col_cd_counterparty character varying, col_cd_reference character varying, col_cd_date date, col_cd_subject character varying, col_cd_currency character varying, col_cd_ceiling numeric, col_cd_vat_rate character varying, col_cd_deadline character varying, col_cd_scheme character varying, col_cd_act character varying, col_cd_comment character varying, col_book_q1 numeric, col_book_q2 numeric, col_book_q3 numeric, col_book_q4 numeric, col_book_next_q1 numeric, col_book_next_q2 numeric, col_book_next_q3 numeric, col_book_next_q4 numeric, col_q1_adj_current numeric, col_q1_adj_ssp numeric, col_q1_adj_reserve numeric, col_q1_adj_comment character varying, col_q1_corrected_plan numeric, col_q1_pay_date date, col_q1_pay_amount numeric, col_q1_pay_comment character varying, col_q1_pay_act character varying, col_q1_booking numeric, col_q1_actual_m1 numeric, col_q1_actual_m2 numeric, col_q1_actual_m3 numeric, col_q1_actual_quarter numeric, col_q1_residual_after_booking numeric, col_q1_residual_after_actual numeric, col_q1_transfer_q2 numeric, col_q1_transfer_q2_delay_acts numeric, col_q1_transfer_q2_delay_procurement numeric, col_q1_transfer_q2_economy_rf numeric, col_q1_transfer_next_comment character varying, col_q1_transfer_q3 numeric, col_q1_transfer_q4 numeric, col_q1_transfer_far_comment character varying, col_q1_transfer_econ numeric, col_q1_total numeric, col_q2_target_change numeric, col_q2_base_correction numeric, col_q2_base_correction_comment character varying, col_q2_revision_inc numeric, col_q2_revision_seq numeric, col_q2_revision_comment character varying, col_q2_new_plan numeric, col_q2_adj_current numeric, col_q2_adj_ssp numeric, col_q2_adj_reserve numeric, col_q2_adj_comment character varying, col_q2_corrected_plan numeric, col_q2_pay_date date, col_q2_pay_amount numeric, col_q2_pay_comment character varying, col_q2_pay_act character varying, col_q2_booking numeric, col_q2_actual_m1 numeric, col_q2_actual_m2 numeric, col_q2_actual_m3 numeric, col_q2_actual_quarter numeric, col_q2_residual_after_booking numeric, col_q2_residual_after_actual numeric, col_q2_transfer_q3 numeric, col_q2_transfer_q3_delay_acts numeric, col_q2_transfer_q3_delay_procurement numeric, col_q2_transfer_q3_economy_rf numeric, col_q2_transfer_next_comment character varying, col_q2_transfer_q4 numeric, col_q2_transfer_far_comment character varying, col_q2_transfer_econ numeric, col_q2_total numeric, col_q3_target_change numeric, col_q3_base_correction numeric, col_q3_base_correction_comment character varying, col_q3_revision_inc numeric, col_q3_revision_seq numeric, col_q3_revision_comment character varying, col_q3_new_plan numeric, col_q3_adj_current numeric, col_q3_adj_ssp numeric, col_q3_adj_reserve numeric, col_q3_adj_comment character varying, col_q3_corrected_plan numeric, col_q3_pay_date date, col_q3_pay_amount numeric, col_q3_pay_comment character varying, col_q3_pay_act character varying, col_q3_booking numeric, col_q3_actual_m1 numeric, col_q3_actual_m2 numeric, col_q3_actual_m3 numeric, col_q3_actual_quarter numeric, col_q3_residual_after_booking numeric, col_q3_residual_after_actual numeric, col_q3_transfer_q4 numeric, col_q3_transfer_q4_delay_acts numeric, col_q3_transfer_q4_delay_procurement numeric, col_q3_transfer_q4_economy_rf numeric, col_q3_transfer_next_comment character varying, col_q3_transfer_econ numeric, col_q3_total numeric, col_q4_target_change numeric, col_q4_base_correction numeric, col_q4_base_correction_comment character varying, col_q4_revision_inc numeric, col_q4_revision_seq numeric, col_q4_revision_comment character varying, col_q4_new_plan numeric, col_q4_adj_current numeric, col_q4_adj_ssp numeric, col_q4_adj_reserve numeric, col_q4_adj_comment character varying, col_q4_corrected_plan numeric, col_q4_pay_date date, col_q4_pay_amount numeric, col_q4_pay_comment character varying, col_q4_pay_act character varying, col_q4_booking numeric, col_q4_actual_m1 numeric, col_q4_actual_m2 numeric, col_q4_actual_m3 numeric, col_q4_actual_spod numeric, col_q4_actual_quarter numeric, col_q4_residual_after_booking numeric, col_q4_residual_after_actual numeric, col_q4_transfer_econ numeric, col_q4_total numeric, col_fact_year numeric, col_pay_year numeric, col_economy_year numeric, col_internal_order character varying, col_cs_total numeric, col_cs_counterparty character varying, col_cs_deadline character varying, col_cs_comment character varying, col_cs_future_y1 numeric, col_cs_future_y2 numeric, col_cs_other_ssp numeric, col_al_contract_ref character varying, col_al_purpose character varying, col_al_order character varying, col_al_property character varying, col_ca_approved numeric, col_ca_protocol character varying, col_ca_note character varying, col_ckk_ceiling numeric, col_ckk_rf_sched character varying, col_ckk_deadline character varying, col_ckk_proc_plan character varying, col_ckk_proc_meth character varying, col_ckk_comment character varying, col_cd_addenda character varying, col_cd_exp_q1 numeric, col_cd_exp_q2 numeric, col_cd_exp_q3 numeric, col_cd_exp_q4 numeric, col_cd_rf_sched character varying, col_cd_exchange_rate numeric, col_cd_amount_foreign numeric, col_q1_adj_rf numeric, col_q1_pay_ho numeric, col_q1_pay_rf numeric, col_q2_adj_rf numeric, col_q2_pay_ho numeric, col_q2_pay_rf numeric, col_q3_adj_rf numeric, col_q3_pay_ho numeric, col_q3_pay_rf numeric, col_q4_adj_rf numeric, col_q4_pay_ho numeric, col_q4_pay_rf numeric, _sort_path integer[]) + LANGUAGE plpgsql + STABLE +AS $function$ +#variable_conflict use_column +DECLARE + s_plan BOOL; s_seq_d BOOL; s_seq_s BOOL; s_appr BOOL; + s_cd BOOL; s_book BOOL; + s_cs BOOL; s_al BOOL; s_ca BOOL; s_ckk BOOL; -- НОВЫЕ блоки + s_q1 BOOL; s_q2 BOOL; s_q3 BOOL; s_q4 BOOL; s_tot BOOL; + s_need_ap BOOL; -- plan/seq/reserve нужны для approved + cp1..cp4 +BEGIN + s_plan := p_sections IS NULL OR 'plan' = ANY(p_sections); + s_seq_d := p_sections IS NULL OR 'seq_dfip' = ANY(p_sections); + s_seq_s := p_sections IS NULL OR 'seq_ssp' = ANY(p_sections); + s_appr := p_sections IS NULL OR 'approved' = ANY(p_sections); + s_cd := p_sections IS NULL OR 'contract' = ANY(p_sections); + s_book := p_sections IS NULL OR 'booking' = ANY(p_sections); + s_cs := p_sections IS NULL OR 'contract_summary' = ANY(p_sections); + s_al := p_sections IS NULL OR 'allocation' = ANY(p_sections); + s_ca := p_sections IS NULL OR 'collegial' = ANY(p_sections); + s_ckk := p_sections IS NULL OR 'ckk' = ANY(p_sections); + s_q1 := p_sections IS NULL OR 'q1' = ANY(p_sections); + s_q2 := p_sections IS NULL OR 'q2' = ANY(p_sections); + s_q3 := p_sections IS NULL OR 'q3' = ANY(p_sections); + s_q4 := p_sections IS NULL OR 'q4' = ANY(p_sections); + s_tot := p_sections IS NULL OR 'totals' = ANY(p_sections); + s_need_ap := s_plan OR s_appr OR s_q1 OR s_q2 OR s_q3 OR s_q4; + + RETURN QUERY + WITH + tw AS ( + SELECT t.id, t.section_code, t.item_id, t.num_group_id, t.name, + t.depth, t.path, t.parent_id, t.parent_item_id, t.desc_ids, + p.parent_item_id AS grandparent_item_id + FROM v3.mv_expense_item_tree t + LEFT JOIN v3.mv_expense_item_tree p ON p.id = t.parent_id + JOIN v3.expense_item_form_type eft ON eft.expense_item_id = t.id + JOIN v3.budget_form bf ON bf.id = p_form_id AND bf.form_type_code = eft.form_type_code + WHERE t.sheet = p_sheet + ), + page AS ( + SELECT bl.id AS lid, bl.expense_item_id AS eid, bl.name AS bname, bl.vsp_id, + bl.internal_order AS io + FROM v3.budget_line bl + JOIN v3.expense_item ei ON ei.id = bl.expense_item_id + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet + ), + jp AS (SELECT p.* FROM v3.plan p JOIN page pg ON p.line_id = pg.lid WHERE s_need_ap), + jsd AS (SELECT s.* FROM v3.sequestration s JOIN page pg ON s.line_id = pg.lid WHERE s_need_ap AND s.actor='DFIP'), + jsg AS (SELECT s.* FROM v3.sequestration s JOIN page pg ON s.line_id = pg.lid WHERE s_need_ap AND s.actor='SSP_GO'), + jr AS (SELECT r.* FROM v3.reserve r JOIN page pg ON r.line_id = pg.lid WHERE s_need_ap), + jcd AS (SELECT c.* FROM v3.contract_detail c JOIN page pg ON c.line_id = pg.lid WHERE s_cd), + jck AS (SELECT c.* FROM v3.ckk c JOIN page pg ON c.line_id = pg.lid WHERE s_book OR s_ckk), + jcs AS (SELECT c.* FROM v3.contract_summary c JOIN page pg ON c.line_id = pg.lid WHERE s_cs), + jal AS (SELECT a.* FROM v3.allocation a JOIN page pg ON a.line_id = pg.lid WHERE s_al), + jca AS (SELECT c.* FROM v3.collegial_approval c JOIN page pg ON c.line_id = pg.lid WHERE s_ca), + jq1 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE s_q1 AND q.quarter=1), + jq2 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE s_q2 AND q.quarter=2), + jq3 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE s_q3 AND q.quarter=3), + jq4 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page pg ON q.line_id = pg.lid WHERE (s_q4 OR s_tot) AND q.quarter=4), + + input_base AS ( + SELECT + pg.lid, pg.eid, pg.bname, pg.vsp_id, + v.address AS vsp_addr, + CASE + WHEN t.depth < 3 THEN t.item_id + WHEN t.depth = 3 THEN t.parent_item_id + WHEN t.depth = 4 THEN t.grandparent_item_id + END AS sc, + t.item_id AS ic, t.num_group_id AS ng, t.name AS ename, t.path AS tree_path, + COALESCE(p.plan_q1,0) AS pq1, COALESCE(p.plan_q2,0) AS pq2, + COALESCE(p.plan_q3,0) AS pq3, COALESCE(p.plan_q4,0) AS pq4, + p.comment AS pcmt, + COALESCE(sd.adj_q1,0) AS dq1, COALESCE(sd.adj_q2,0) AS dq2, + COALESCE(sd.adj_q3,0) AS dq3, COALESCE(sd.adj_q4,0) AS dq4, + sd.justification AS djust, + COALESCE(sg.adj_q1,0) AS gq1, COALESCE(sg.adj_q2,0) AS gq2, + COALESCE(sg.adj_q3,0) AS gq3, COALESCE(sg.adj_q4,0) AS gq4, + sg.justification AS gjust, + COALESCE(r.amount_q1,0) AS rq1, COALESCE(r.amount_q2,0) AS rq2, + COALESCE(r.amount_q3,0) AS rq3, COALESCE(r.amount_q4,0) AS rq4, + cd.counterparty AS cd_cp, cd.reference AS cd_ref, cd.contract_date AS cd_dt, + cd.subject AS cd_subj, cd.currency AS cd_cur, cd.ceiling_amount AS cd_ceil, + cd.vat_rate AS cd_vat, cd.deadline AS cd_dl, cd.payment_scheme AS cd_sch, + cd.act AS cd_act, cd.comment AS cd_cmt, + ck.expenses_q1 AS bk1, ck.expenses_q2 AS bk2, ck.expenses_q3 AS bk3, ck.expenses_q4 AS bk4, + ck.expenses_next_year_q1 AS bn1, ck.expenses_next_year_q2 AS bn2, + ck.expenses_next_year_q3 AS bn3, ck.expenses_next_year_q4 AS bn4, + pg.io AS io, + cs.total_amount AS cs_total, cs.counterparty AS cs_cp, cs.deadline AS cs_dl, + cs.comment AS cs_cmt, cs.future_payments_y1 AS cs_fy1, cs.future_payments_y2 AS cs_fy2, + cs.other_ssp_amount AS cs_oth, + al.contract_ref AS al_cref, al.allocation_purpose AS al_purp, + al.internal_order AS al_io, al.property_object AS al_prop, + ca.approved_amount AS ca_amt, ca.protocol_reference AS ca_proto, ca.note AS ca_note, + ck.ceiling_amount AS ckk_ceil, ck.rf_schedule AS ckk_rf, ck.delivery_deadline AS ckk_dl, + ck.procurement_plan AS ckk_pplan, ck.procurement_method AS ckk_pmeth, ck.comment AS ckk_cmt, + cd.addenda AS cd_add, cd.expenses_q1 AS cd_e1, cd.expenses_q2 AS cd_e2, + cd.expenses_q3 AS cd_e3, cd.expenses_q4 AS cd_e4, cd.rf_schedule AS cd_rf, + cd.exchange_rate AS cd_rate, cd.amount_foreign AS cd_fx, + q1.adj_rf AS q1_arf, q1.payment_amount_ho AS q1_pho, q1.payment_amount_rf AS q1_prf, + q2.adj_rf AS q2_arf, q2.payment_amount_ho AS q2_pho, q2.payment_amount_rf AS q2_prf, + q3.adj_rf AS q3_arf, q3.payment_amount_ho AS q3_pho, q3.payment_amount_rf AS q3_prf, + q4.adj_rf AS q4_arf, q4.payment_amount_ho AS q4_pho, q4.payment_amount_rf AS q4_prf, + q1.adj_current AS q1_ac, q1.adj_ssp AS q1_as, q1.adj_reserve AS q1_arv, q1.adj_comment AS q1_acmt, + q1.payment_date AS q1_pd, q1.payment_amount AS q1_pa, + q1.payment_comment AS q1_pcmt, + q1.payment_act AS q1_pact, + COALESCE( + q1.booking_amount, + CASE + WHEN p_sheet = 'AHR' THEN ck.expenses_q1 + WHEN ck.expenses_q1 > 0 THEN ck.expenses_q1 + ELSE cd.expenses_q1 + END + + ) AS q1_book, + q1.actual_m1 AS q1_m1, q1.actual_m2 AS q1_m2, q1.actual_m3 AS q1_m3, + q1.transfer_to_q2 AS q1_tq2, q1.transfer_to_q3 AS q1_tq3, q1.transfer_to_q4 AS q1_tq4, + q1.transfer_to_economy AS q1_te, + q1.transfer_delay_acts AS q1_tda, q1.transfer_delay_procurement AS q1_tdp, + q1.transfer_economy_rf AS q1_terf, + q1.transfer_next_comment AS q1_tnc, q1.transfer_far_comment AS q1_tfc, + q2.target_change AS q2_tc, q2.base_plan_correction AS q2_bc, q2.base_plan_correction_comment AS q2_bcc, + q2.plan_revision_increase AS q2_rinc, q2.plan_revision_sequester AS q2_rseq, + q2.plan_revision_comment AS q2_rcmt, + q2.adj_current AS q2_ac, q2.adj_ssp AS q2_as, q2.adj_reserve AS q2_arv, + q2.adj_comment AS q2_acmt, + q2.payment_date AS q2_pd, q2.payment_amount AS q2_pa, + q2.payment_comment AS q2_pcmt, q2.payment_act AS q2_pact, + COALESCE( + q2.booking_amount, + CASE + WHEN p_sheet = 'AHR' THEN ck.expenses_q2 + WHEN ck.expenses_q2 > 0 THEN ck.expenses_q2 + ELSE cd.expenses_q2 + END + + ) AS q2_book, + q2.actual_m1 AS q2_m1, q2.actual_m2 AS q2_m2, q2.actual_m3 AS q2_m3, + q2.transfer_to_q3 AS q2_tq3, q2.transfer_to_q4 AS q2_tq4, q2.transfer_to_economy AS q2_te, + q2.transfer_delay_acts AS q2_tda, q2.transfer_delay_procurement AS q2_tdp, + q2.transfer_economy_rf AS q2_terf, + q2.transfer_next_comment AS q2_tnc, q2.transfer_far_comment AS q2_tfc, + q3.target_change AS q3_tc, q3.base_plan_correction AS q3_bc, + q3.base_plan_correction_comment AS q3_bcc, + q3.plan_revision_increase AS q3_rinc, q3.plan_revision_sequester AS q3_rseq, + q3.plan_revision_comment AS q3_rcmt, + q3.adj_current AS q3_ac, q3.adj_ssp AS q3_as, q3.adj_reserve AS q3_arv, + q3.adj_comment AS q3_acmt, + q3.payment_date AS q3_pd, q3.payment_amount AS q3_pa, + q3.payment_comment AS q3_pcmt, q3.payment_act AS q3_pact, + COALESCE( + q3.booking_amount, + CASE + WHEN p_sheet = 'AHR' THEN ck.expenses_q3 + WHEN ck.expenses_q3 > 0 THEN ck.expenses_q3 + ELSE cd.expenses_q3 + END + + ) AS q3_book, + q3.actual_m1 AS q3_m1, q3.actual_m2 AS q3_m2, q3.actual_m3 AS q3_m3, + q3.transfer_to_q4 AS q3_tq4, q3.transfer_to_economy AS q3_te, + q3.transfer_delay_acts AS q3_tda, q3.transfer_delay_procurement AS q3_tdp, + q3.transfer_economy_rf AS q3_terf, q3.transfer_next_comment AS q3_tnc, + q4.target_change AS q4_tc, q4.base_plan_correction AS q4_bc, + q4.base_plan_correction_comment AS q4_bcc, + q4.plan_revision_increase AS q4_rinc, q4.plan_revision_sequester AS q4_rseq, + q4.plan_revision_comment AS q4_rcmt, + q4.adj_current AS q4_ac, q4.adj_ssp AS q4_as, q4.adj_reserve AS q4_arv, + q4.adj_comment AS q4_acmt, + q4.payment_date AS q4_pd, q4.payment_amount AS q4_pa, + q4.payment_comment AS q4_pcmt, q4.payment_act AS q4_pact, + COALESCE( + q4.booking_amount, + CASE + WHEN p_sheet = 'AHR' THEN ck.expenses_q4 + WHEN ck.expenses_q4 > 0 THEN ck.expenses_q4 + ELSE cd.expenses_q4 + END + + ) AS q4_book, + q4.actual_m1 AS q4_m1, q4.actual_m2 AS q4_m2, q4.actual_m3 AS q4_m3, q4.actual_spod AS q4_spod, + q4.transfer_to_economy AS q4_te, + COALESCE(p.plan_q1,0)+COALESCE(sd.adj_q1,0)+COALESCE(sg.adj_q1,0)+COALESCE(r.amount_q1,0) AS ap1, + COALESCE(p.plan_q2,0)+COALESCE(sd.adj_q2,0)+COALESCE(sg.adj_q2,0)+COALESCE(r.amount_q2,0) AS ap2, + COALESCE(p.plan_q3,0)+COALESCE(sd.adj_q3,0)+COALESCE(sg.adj_q3,0)+COALESCE(r.amount_q3,0) AS ap3, + COALESCE(p.plan_q4,0)+COALESCE(sd.adj_q4,0)+COALESCE(sg.adj_q4,0)+COALESCE(r.amount_q4,0) AS ap4 + FROM page pg + JOIN tw t ON t.id = pg.eid + LEFT JOIN v3.vsp v ON v.id = pg.vsp_id + LEFT JOIN jp p ON p.line_id = pg.lid + LEFT JOIN jsd sd ON sd.line_id = pg.lid + LEFT JOIN jsg sg ON sg.line_id = pg.lid + LEFT JOIN jr r ON r.line_id = pg.lid + LEFT JOIN jcd cd ON cd.line_id = pg.lid + LEFT JOIN jck ck ON ck.line_id = pg.lid + LEFT JOIN jcs cs ON cs.line_id = pg.lid + LEFT JOIN jal al ON al.line_id = pg.lid + LEFT JOIN jca ca ON ca.line_id = pg.lid + LEFT JOIN jq1 q1 ON q1.line_id = pg.lid + LEFT JOIN jq2 q2 ON q2.line_id = pg.lid + LEFT JOIN jq3 q3 ON q3.line_id = pg.lid + LEFT JOIN jq4 q4 ON q4.line_id = pg.lid + ), + enriched AS ( + SELECT b.*, + b.ap1 + COALESCE(b.q1_ac,0)+COALESCE(b.q1_as,0)+COALESCE(b.q1_arv,0) AS cp1, + COALESCE(b.q1_m1,0)+COALESCE(b.q1_m2,0)+COALESCE(b.q1_m3,0) AS aq1, + b.ap2 + COALESCE(b.q1_tq2,0) + COALESCE(b.q2_rinc,0)+COALESCE(b.q2_rseq,0) + COALESCE(b.q2_bc,0) AS np2, + COALESCE(b.q2_m1,0)+COALESCE(b.q2_m2,0)+COALESCE(b.q2_m3,0) AS aq2, + b.ap3 + COALESCE(b.q1_tq3,0)+COALESCE(b.q2_tq3,0) + COALESCE(b.q3_rinc,0)+COALESCE(b.q3_rseq,0) + COALESCE(b.q3_bc,0) AS np3, + COALESCE(b.q3_m1,0)+COALESCE(b.q3_m2,0)+COALESCE(b.q3_m3,0) AS aq3, + b.ap4 + COALESCE(b.q1_tq4,0)+COALESCE(b.q2_tq4,0)+COALESCE(b.q3_tq4,0) + COALESCE(b.q4_rinc,0)+COALESCE(b.q4_rseq,0) + COALESCE(b.q4_bc,0) AS np4, + COALESCE(b.q4_m1,0)+COALESCE(b.q4_m2,0)+COALESCE(b.q4_m3,0)+COALESCE(b.q4_spod,0) AS aq4 + FROM input_base b + ), + final_input AS ( + SELECT e.*, + e.np2 + COALESCE(e.q2_ac,0)+COALESCE(e.q2_as,0)+COALESCE(e.q2_arv,0) AS cp2, + e.np3 + COALESCE(e.q3_ac,0)+COALESCE(e.q3_as,0)+COALESCE(e.q3_arv,0) AS cp3, + e.np4 + COALESCE(e.q4_ac,0)+COALESCE(e.q4_as,0)+COALESCE(e.q4_arv,0) AS cp4 + FROM enriched e + ), + + agg AS ( + SELECT bl.expense_item_id AS eid, + SUM(p.plan_q1) AS sp1, SUM(p.plan_q2) AS sp2, SUM(p.plan_q3) AS sp3, SUM(p.plan_q4) AS sp4, + SUM(sd.adj_q1) AS sd1, SUM(sd.adj_q2) AS sd2, SUM(sd.adj_q3) AS sd3, SUM(sd.adj_q4) AS sd4, + SUM(sg.adj_q1) AS sg1, SUM(sg.adj_q2) AS sg2, SUM(sg.adj_q3) AS sg3, SUM(sg.adj_q4) AS sg4, + SUM(r.amount_q1) AS sr1, SUM(r.amount_q2) AS sr2, SUM(r.amount_q3) AS sr3, SUM(r.amount_q4) AS sr4, + SUM(ck.expenses_q1) AS bk1, SUM(ck.expenses_q2) AS bk2, SUM(ck.expenses_q3) AS bk3, SUM(ck.expenses_q4) AS bk4, + SUM(cd.expenses_q1) AS bkc1, SUM(cd.expenses_q2) AS bkc2, SUM(cd.expenses_q3) AS bkc3, SUM(cd.expenses_q4) AS bkc4, + SUM(ck.expenses_next_year_q1) AS bn1, SUM(ck.expenses_next_year_q2) AS bn2, + SUM(ck.expenses_next_year_q3) AS bn3, SUM(ck.expenses_next_year_q4) AS bn4 + FROM v3.budget_line bl + JOIN v3.expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN v3.plan p ON p.line_id = bl.id AND s_need_ap + LEFT JOIN v3.sequestration sd ON sd.line_id = bl.id AND sd.actor='DFIP' AND s_need_ap + LEFT JOIN v3.sequestration sg ON sg.line_id = bl.id AND sg.actor='SSP_GO' AND s_need_ap + LEFT JOIN v3.reserve r ON r.line_id = bl.id AND s_need_ap + LEFT JOIN v3.ckk ck ON ck.line_id = bl.id AND s_book + LEFT JOIN v3.contract_detail cd ON cd.line_id = bl.id AND s_cd + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet + AND (s_need_ap OR s_book OR s_cd) + GROUP BY bl.expense_item_id + ), + aq_q AS ( + SELECT bl.expense_item_id AS eid, qrtrs.qrtr as quarter, + SUM(q.adj_current) AS ac, SUM(q.adj_ssp) AS as_v, SUM(q.adj_reserve) AS arv, + SUM(q.payment_amount) AS pa, + SUM( + COALESCE( + q.booking_amount, + CASE + WHEN qrtrs.qrtr = 1 + THEN CASE + WHEN p_sheet = 'AHR' THEN ck.expenses_q1 + WHEN ck.expenses_q1 > 0 THEN ck.expenses_q1 + ELSE cd.expenses_q1 + END + WHEN qrtrs.qrtr = 2 THEN CASE + WHEN p_sheet = 'AHR' THEN ck.expenses_q2 + WHEN ck.expenses_q2 > 0 THEN ck.expenses_q2 + ELSE cd.expenses_q2 + END + WHEN qrtrs.qrtr = 3 THEN CASE + WHEN p_sheet = 'AHR' THEN ck.expenses_q3 + WHEN ck.expenses_q3 > 0 THEN ck.expenses_q3 + ELSE cd.expenses_q3 + END + WHEN qrtrs.qrtr = 4 THEN CASE + WHEN p_sheet = 'AHR' THEN ck.expenses_q4 + WHEN ck.expenses_q4 > 0 THEN ck.expenses_q4 + ELSE cd.expenses_q4 + END + END + + )) AS bk, + SUM(q.actual_m1) AS m1, SUM(q.actual_m2) AS m2, SUM(q.actual_m3) AS m3, + SUM(q.actual_spod) AS spod, + SUM(q.transfer_to_q2) AS tq2, SUM(q.transfer_to_q3) AS tq3, SUM(q.transfer_to_q4) AS tq4, + SUM(q.transfer_to_economy) AS te, + SUM(q.transfer_delay_acts) AS tda, + SUM(q.transfer_delay_procurement) AS tdp, + SUM(q.transfer_economy_rf) AS terf, + SUM(COALESCE(q.plan_revision_increase,0) + + COALESCE(q.plan_revision_sequester,0) + + COALESCE(q.target_change,0) + + COALESCE(q.base_plan_correction,0)) AS rev + FROM v3.budget_line bl + CROSS JOIN (VALUES (1), (2), (3), (4)) AS qrtrs(qrtr) + + JOIN v3.expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN v3.budget_line_quarter q ON q.line_id = bl.id AND q.quarter = qrtrs.qrtr + LEFT JOIN v3.ckk ck ON bl.id = ck.line_id + LEFT JOIN v3.contract_detail cd ON bl.id = cd.line_id + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet + GROUP BY bl.expense_item_id, qrtrs.qrtr + ), + tw_agg AS ( + SELECT tw.id, + SUM(a.sp1) AS sp1, SUM(a.sp2) AS sp2, SUM(a.sp3) AS sp3, SUM(a.sp4) AS sp4, + SUM(a.sd1) AS sd1, SUM(a.sd2) AS sd2, SUM(a.sd3) AS sd3, SUM(a.sd4) AS sd4, + SUM(a.sg1) AS sg1, SUM(a.sg2) AS sg2, SUM(a.sg3) AS sg3, SUM(a.sg4) AS sg4, + SUM(a.sr1) AS sr1, SUM(a.sr2) AS sr2, SUM(a.sr3) AS sr3, SUM(a.sr4) AS sr4, + SUM(a.bk1) AS bk1, SUM(a.bk2) AS bk2, SUM(a.bk3) AS bk3, SUM(a.bk4) AS bk4, + SUM(a.bkc1) AS bkc1, SUM(a.bkc2) AS bkc2, SUM(a.bkc3) AS bkc3, SUM(a.bkc4) AS bkc4, + SUM(a.bn1) AS bn1, SUM(a.bn2) AS bn2, SUM(a.bn3) AS bn3, SUM(a.bn4) AS bn4 + FROM tw LEFT JOIN agg a ON a.eid = ANY(tw.desc_ids) + GROUP BY tw.id + ), + tw_aq1 AS ( + SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.bk) AS bk, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, + SUM(b.tq2) AS tq2, SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4, SUM(b.te) AS te, + SUM(b.tda) AS tda, SUM(b.tdp) AS tdp, SUM(b.terf) AS terf + FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=1 GROUP BY tw.id + ), + tw_aq2 AS ( + SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.bk) AS bk, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, + SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4, SUM(b.te) AS te, SUM(b.rev) AS rev, + SUM(b.tda) AS tda, SUM(b.tdp) AS tdp, SUM(b.terf) AS terf + FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=2 GROUP BY tw.id + ), + tw_aq3 AS ( + SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.bk) AS bk, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, + SUM(b.tq4) AS tq4, SUM(b.te) AS te, SUM(b.rev) AS rev, + SUM(b.tda) AS tda, SUM(b.tdp) AS tdp, SUM(b.terf) AS terf + FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=3 GROUP BY tw.id + ), + tw_aq4 AS ( + SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.bk) AS bk, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, SUM(b.spod) AS spod, + SUM(b.te) AS te, SUM(b.rev) AS rev + FROM tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=4 GROUP BY tw.id + ) + + SELECT * FROM ( + SELECT + 'INPUT'::VARCHAR, array_length(f.tree_path, 1)::INT, f.lid::INT, + f.sc, f.ic, f.ng, COALESCE(f.bname, f.ename), f.vsp_id, f.vsp_addr, + CASE WHEN s_plan THEN f.pq1 END, CASE WHEN s_plan THEN f.pq2 END, + CASE WHEN s_plan THEN f.pq3 END, CASE WHEN s_plan THEN f.pq4 END, + CASE WHEN s_plan THEN f.pq1+f.pq2+f.pq3+f.pq4 END, CASE WHEN s_plan THEN f.pcmt END, + CASE WHEN s_seq_d THEN f.dq1 END, CASE WHEN s_seq_d THEN f.dq2 END, + CASE WHEN s_seq_d THEN f.dq3 END, CASE WHEN s_seq_d THEN f.dq4 END, + CASE WHEN s_seq_d THEN f.dq1+f.dq2+f.dq3+f.dq4 END, CASE WHEN s_seq_d THEN f.djust END, + CASE WHEN s_seq_s THEN f.gq1 END, CASE WHEN s_seq_s THEN f.gq2 END, + CASE WHEN s_seq_s THEN f.gq3 END, CASE WHEN s_seq_s THEN f.gq4 END, + CASE WHEN s_seq_s THEN f.gq1+f.gq2+f.gq3+f.gq4 END, CASE WHEN s_seq_s THEN f.gjust END, + CASE WHEN s_appr THEN f.ap1 END, CASE WHEN s_appr THEN f.ap2 END, + CASE WHEN s_appr THEN f.ap3 END, CASE WHEN s_appr THEN f.ap4 END, + CASE WHEN s_appr THEN f.ap1+f.ap2+f.ap3+f.ap4 END, + CASE WHEN s_cd THEN f.cd_cp END, CASE WHEN s_cd THEN f.cd_ref END, + CASE WHEN s_cd THEN f.cd_dt END, CASE WHEN s_cd THEN f.cd_subj END, + CASE WHEN s_cd THEN f.cd_cur END, CASE WHEN s_cd THEN f.cd_ceil END, + CASE WHEN s_cd THEN f.cd_vat END, CASE WHEN s_cd THEN f.cd_dl END, + CASE WHEN s_cd THEN f.cd_sch END, CASE WHEN s_cd THEN f.cd_act END, + CASE WHEN s_cd THEN f.cd_cmt END, + CASE WHEN s_book THEN f.bk1 END, CASE WHEN s_book THEN f.bk2 END, + CASE WHEN s_book THEN f.bk3 END, CASE WHEN s_book THEN f.bk4 END, + CASE WHEN s_book THEN f.bn1 END, CASE WHEN s_book THEN f.bn2 END, + CASE WHEN s_book THEN f.bn3 END, CASE WHEN s_book THEN f.bn4 END, + CASE WHEN s_q1 THEN f.q1_ac END, CASE WHEN s_q1 THEN f.q1_as END, CASE WHEN s_q1 THEN f.q1_arv END, + CASE WHEN s_q1 THEN f.q1_acmt END, CASE WHEN s_q1 THEN f.cp1 END, + CASE WHEN s_q1 THEN f.q1_pd END, CASE WHEN s_q1 THEN f.q1_pa END, + CASE WHEN s_q1 THEN f.q1_pcmt END, + CASE WHEN s_q1 THEN f.q1_pact END, CASE WHEN s_q1 THEN f.q1_book END, + CASE WHEN s_q1 THEN f.q1_m1 END, CASE WHEN s_q1 THEN f.q1_m2 END, CASE WHEN s_q1 THEN f.q1_m3 END, + CASE WHEN s_q1 THEN f.aq1 END, + CASE WHEN s_q1 THEN f.cp1 - COALESCE(f.q1_book,0) END, -- residual_after_booking + CASE WHEN s_q1 THEN f.cp1 - f.aq1 END, -- residual_after_actual + CASE WHEN s_q1 THEN f.q1_tq2 END, + CASE WHEN s_q1 THEN f.q1_tda END, CASE WHEN s_q1 THEN f.q1_tdp END, CASE WHEN s_q1 THEN f.q1_terf END, + CASE WHEN s_q1 THEN f.q1_tnc END, + CASE WHEN s_q1 THEN f.q1_tq3 END, CASE WHEN s_q1 THEN f.q1_tq4 END, + CASE WHEN s_q1 THEN f.q1_tfc END, + CASE WHEN s_q1 THEN f.q1_te END, + CASE WHEN s_q1 THEN + COALESCE(f.q1_tda,0)+COALESCE(f.q1_tdp,0)+COALESCE(f.q1_terf,0) + +COALESCE(f.q1_tq3,0)+COALESCE(f.q1_tq4,0)+COALESCE(f.q1_te,0) + END, + CASE WHEN s_q2 THEN f.q2_tc END, CASE WHEN s_q2 THEN f.q2_bc END, CASE WHEN s_q2 THEN f.q2_bcc END, + CASE WHEN s_q2 THEN f.q2_rinc END, CASE WHEN s_q2 THEN f.q2_rseq END, + CASE WHEN s_q2 THEN f.q2_rcmt END, + CASE WHEN s_q2 THEN f.np2 END, + CASE WHEN s_q2 THEN f.q2_ac END, CASE WHEN s_q2 THEN f.q2_as END, CASE WHEN s_q2 THEN f.q2_arv END, + CASE WHEN s_q2 THEN f.q2_acmt END, + CASE WHEN s_q2 THEN f.cp2 END, + CASE WHEN s_q2 THEN f.q2_pd END, CASE WHEN s_q2 THEN f.q2_pa END, + CASE WHEN s_q2 THEN f.q2_pcmt END, CASE WHEN s_q2 THEN f.q2_pact END, + CASE WHEN s_q2 THEN f.q2_book END, + CASE WHEN s_q2 THEN f.q2_m1 END, CASE WHEN s_q2 THEN f.q2_m2 END, CASE WHEN s_q2 THEN f.q2_m3 END, + CASE WHEN s_q2 THEN f.aq2 END, + CASE WHEN s_q2 THEN f.cp2 - COALESCE(f.q2_book,0) END, + CASE WHEN s_q2 THEN f.cp2 - f.aq2 END, + CASE WHEN s_q2 THEN f.q2_tq3 END, + CASE WHEN s_q2 THEN f.q2_tda END, CASE WHEN s_q2 THEN f.q2_tdp END, CASE WHEN s_q2 THEN f.q2_terf END, + CASE WHEN s_q2 THEN f.q2_tnc END, + CASE WHEN s_q2 THEN f.q2_tq4 END, + CASE WHEN s_q2 THEN f.q2_tfc END, + CASE WHEN s_q2 THEN f.q2_te END, + CASE WHEN s_q2 THEN + COALESCE(f.q2_tda,0)+COALESCE(f.q2_tdp,0)+COALESCE(f.q2_terf,0) + +COALESCE(f.q2_tq4,0)+COALESCE(f.q2_te,0) + END, + CASE WHEN s_q3 THEN f.q3_tc END, CASE WHEN s_q3 THEN f.q3_bc END, + CASE WHEN s_q3 THEN f.q3_bcc END, + CASE WHEN s_q3 THEN f.q3_rinc END, CASE WHEN s_q3 THEN f.q3_rseq END, + CASE WHEN s_q3 THEN f.q3_rcmt END, + CASE WHEN s_q3 THEN f.np3 END, + CASE WHEN s_q3 THEN f.q3_ac END, CASE WHEN s_q3 THEN f.q3_as END, CASE WHEN s_q3 THEN f.q3_arv END, + CASE WHEN s_q3 THEN f.q3_acmt END, + CASE WHEN s_q3 THEN f.cp3 END, + CASE WHEN s_q3 THEN f.q3_pd END, CASE WHEN s_q3 THEN f.q3_pa END, + CASE WHEN s_q3 THEN f.q3_pcmt END, CASE WHEN s_q3 THEN f.q3_pact END, + CASE WHEN s_q3 THEN f.q3_book END, + CASE WHEN s_q3 THEN f.q3_m1 END, CASE WHEN s_q3 THEN f.q3_m2 END, CASE WHEN s_q3 THEN f.q3_m3 END, + CASE WHEN s_q3 THEN f.aq3 END, + CASE WHEN s_q3 THEN f.cp3 - COALESCE(f.q3_book,0) END, + CASE WHEN s_q3 THEN f.cp3 - f.aq3 END, + CASE WHEN s_q3 THEN f.q3_tq4 END, + CASE WHEN s_q3 THEN f.q3_tda END, CASE WHEN s_q3 THEN f.q3_tdp END, CASE WHEN s_q3 THEN f.q3_terf END, + CASE WHEN s_q3 THEN f.q3_tnc END, + CASE WHEN s_q3 THEN f.q3_te END, + CASE WHEN s_q3 THEN + COALESCE(f.q3_tda,0)+COALESCE(f.q3_tdp,0)+COALESCE(f.q3_terf,0) + +COALESCE(f.q3_te,0) + END, + CASE WHEN s_q4 THEN f.q4_tc END, CASE WHEN s_q4 THEN f.q4_bc END, + CASE WHEN s_q4 THEN f.q4_bcc END, + CASE WHEN s_q4 THEN f.q4_rinc END, CASE WHEN s_q4 THEN f.q4_rseq END, + CASE WHEN s_q4 THEN f.q4_rcmt END, + CASE WHEN s_q4 THEN f.np4 END, + CASE WHEN s_q4 THEN f.q4_ac END, CASE WHEN s_q4 THEN f.q4_as END, CASE WHEN s_q4 THEN f.q4_arv END, + CASE WHEN s_q4 THEN f.q4_acmt END, + CASE WHEN s_q4 THEN f.cp4 END, + CASE WHEN s_q4 THEN f.q4_pd END, CASE WHEN s_q4 THEN f.q4_pa END, + CASE WHEN s_q4 THEN f.q4_pcmt END, CASE WHEN s_q4 THEN f.q4_pact END, + CASE WHEN s_q4 THEN f.q4_book END, + CASE WHEN s_q4 THEN f.q4_m1 END, CASE WHEN s_q4 THEN f.q4_m2 END, CASE WHEN s_q4 THEN f.q4_m3 END, + CASE WHEN s_q4 THEN f.q4_spod END, + CASE WHEN s_q4 THEN f.aq4 END, + CASE WHEN s_q4 THEN f.cp4 - COALESCE(f.q4_book,0) END, + CASE WHEN s_q4 THEN f.cp4 - f.aq4 END, + CASE WHEN s_q4 THEN f.q4_te END, + CASE WHEN s_q4 THEN f.q4_te END, + CASE WHEN s_tot THEN f.aq1+f.aq2+f.aq3+f.aq4 END, + CASE WHEN s_tot THEN COALESCE(f.q1_pa,0)+COALESCE(f.q2_pa,0)+COALESCE(f.q3_pa,0)+COALESCE(f.q4_pa,0) END, + CASE WHEN s_tot THEN f.cp4 - f.aq4 END, + + f.io, + CASE WHEN s_cs THEN f.cs_total END, CASE WHEN s_cs THEN f.cs_cp END, + CASE WHEN s_cs THEN f.cs_dl END, CASE WHEN s_cs THEN f.cs_cmt END, + CASE WHEN s_cs THEN f.cs_fy1 END, CASE WHEN s_cs THEN f.cs_fy2 END, + CASE WHEN s_cs THEN f.cs_oth END, + CASE WHEN s_al THEN f.al_cref END, CASE WHEN s_al THEN f.al_purp END, + CASE WHEN s_al THEN f.al_io END, CASE WHEN s_al THEN f.al_prop END, + CASE WHEN s_ca THEN f.ca_amt END, CASE WHEN s_ca THEN f.ca_proto END, + CASE WHEN s_ca THEN f.ca_note END, + CASE WHEN s_ckk THEN f.ckk_ceil END, CASE WHEN s_ckk THEN f.ckk_rf END, + CASE WHEN s_ckk THEN f.ckk_dl END, CASE WHEN s_ckk THEN f.ckk_pplan END, + CASE WHEN s_ckk THEN f.ckk_pmeth END, CASE WHEN s_ckk THEN f.ckk_cmt END, + CASE WHEN s_cd THEN f.cd_add END, CASE WHEN s_cd THEN f.cd_e1 END, + CASE WHEN s_cd THEN f.cd_e2 END, CASE WHEN s_cd THEN f.cd_e3 END, + CASE WHEN s_cd THEN f.cd_e4 END, CASE WHEN s_cd THEN f.cd_rf END, + CASE WHEN s_cd THEN f.cd_rate END, CASE WHEN s_cd THEN f.cd_fx END, + CASE WHEN s_q1 THEN f.q1_arf END, CASE WHEN s_q1 THEN f.q1_pho END, CASE WHEN s_q1 THEN f.q1_prf END, + CASE WHEN s_q2 THEN f.q2_arf END, CASE WHEN s_q2 THEN f.q2_pho END, CASE WHEN s_q2 THEN f.q2_prf END, + CASE WHEN s_q3 THEN f.q3_arf END, CASE WHEN s_q3 THEN f.q3_pho END, CASE WHEN s_q3 THEN f.q3_prf END, + CASE WHEN s_q4 THEN f.q4_arf END, CASE WHEN s_q4 THEN f.q4_pho END, CASE WHEN s_q4 THEN f.q4_prf END, + + f.tree_path || ARRAY[f.lid::INT] AS _sort + FROM final_input f + + UNION ALL + + SELECT + CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' WHEN 4 THEN 'SUB_SUB_ITEM' END::VARCHAR, + t.depth, NULL::INT, + CASE + WHEN t.depth < 3 THEN t.item_id + WHEN t.depth = 3 THEN t.parent_item_id + WHEN t.depth = 4 THEN t.grandparent_item_id + END, + CASE WHEN t.depth >= 3 THEN t.item_id ELSE NULL END, + t.num_group_id, t.name, NULL::INT, NULL::VARCHAR, + CASE WHEN s_plan THEN ta.sp1 END, CASE WHEN s_plan THEN ta.sp2 END, + CASE WHEN s_plan THEN ta.sp3 END, CASE WHEN s_plan THEN ta.sp4 END, + CASE WHEN s_plan THEN COALESCE(ta.sp1,0)+COALESCE(ta.sp2,0)+COALESCE(ta.sp3,0)+COALESCE(ta.sp4,0) END, + NULL::VARCHAR, + CASE WHEN s_seq_d THEN ta.sd1 END, CASE WHEN s_seq_d THEN ta.sd2 END, + CASE WHEN s_seq_d THEN ta.sd3 END, CASE WHEN s_seq_d THEN ta.sd4 END, + CASE WHEN s_seq_d THEN COALESCE(ta.sd1,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sd4,0) END, + NULL::VARCHAR, + CASE WHEN s_seq_s THEN ta.sg1 END, CASE WHEN s_seq_s THEN ta.sg2 END, + CASE WHEN s_seq_s THEN ta.sg3 END, CASE WHEN s_seq_s THEN ta.sg4 END, + CASE WHEN s_seq_s THEN COALESCE(ta.sg1,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sg4,0) END, + NULL::VARCHAR, + CASE WHEN s_appr THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0) END, + CASE WHEN s_appr THEN COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0) END, + CASE WHEN s_appr THEN COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0) END, + CASE WHEN s_appr THEN COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0) END, + CASE WHEN s_appr THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0) + +COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0) + +COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0) + +COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0) END, + NULL::VARCHAR, NULL::VARCHAR, NULL::DATE, NULL::VARCHAR, + NULL::VARCHAR, NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, + NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_book THEN ta.bk1 END, CASE WHEN s_book THEN ta.bk2 END, + CASE WHEN s_book THEN ta.bk3 END, CASE WHEN s_book THEN ta.bk4 END, + CASE WHEN s_book THEN ta.bn1 END, CASE WHEN s_book THEN ta.bn2 END, + CASE WHEN s_book THEN ta.bn3 END, CASE WHEN s_book THEN ta.bn4 END, + CASE WHEN s_q1 THEN tb1.ac END, CASE WHEN s_q1 THEN tb1.as_v END, CASE WHEN s_q1 THEN tb1.arv END, + NULL::VARCHAR, + CASE WHEN s_q1 THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0)+COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arv,0) END, + NULL::DATE, + CASE WHEN s_q1 THEN tb1.pa END, + NULL::VARCHAR, -- pay_comment + NULL::VARCHAR, -- pay_act + CASE WHEN s_q1 THEN tb1.bk END, -- booking (rollup) + CASE WHEN s_q1 THEN tb1.m1 END, CASE WHEN s_q1 THEN tb1.m2 END, CASE WHEN s_q1 THEN tb1.m3 END, + CASE WHEN s_q1 THEN COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0) END, + CASE WHEN s_q1 THEN + (COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0)+COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arv,0)) + - COALESCE(tb1.bk,0) + END, + CASE WHEN s_q1 THEN + (COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0)+COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arv,0)) + - (COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)) + END, + CASE WHEN s_q1 THEN tb1.tq2 END, + CASE WHEN s_q1 THEN tb1.tda END, CASE WHEN s_q1 THEN tb1.tdp END, CASE WHEN s_q1 THEN tb1.terf END, + NULL::VARCHAR, -- transfer_next_comment (text, no rollup) + CASE WHEN s_q1 THEN tb1.tq3 END, CASE WHEN s_q1 THEN tb1.tq4 END, + NULL::VARCHAR, -- far comment + CASE WHEN s_q1 THEN tb1.te END, + CASE WHEN s_q1 THEN + COALESCE(tb1.tda,0)+COALESCE(tb1.tdp,0)+COALESCE(tb1.terf,0) + +COALESCE(tb1.tq3,0)+COALESCE(tb1.tq4,0)+COALESCE(tb1.te,0) + END, + NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- target_change/base_correction + NULL::NUMERIC, NULL::NUMERIC, -- revision_inc/revision_seq + NULL::VARCHAR, -- revision_comment + CASE WHEN s_q2 THEN + COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0) + END, + CASE WHEN s_q2 THEN tb2.ac END, CASE WHEN s_q2 THEN tb2.as_v END, CASE WHEN s_q2 THEN tb2.arv END, + NULL::VARCHAR, -- adj_comment + CASE WHEN s_q2 THEN + COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arv,0) + END, + NULL::DATE, + CASE WHEN s_q2 THEN tb2.pa END, + NULL::VARCHAR, -- pay_comment + NULL::VARCHAR, -- pay_act + CASE WHEN s_q2 THEN tb2.bk END, -- booking (rollup) + CASE WHEN s_q2 THEN tb2.m1 END, CASE WHEN s_q2 THEN tb2.m2 END, CASE WHEN s_q2 THEN tb2.m3 END, + CASE WHEN s_q2 THEN COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0) END, + CASE WHEN s_q2 THEN + (COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arv,0)) + - COALESCE(tb2.bk,0) + END, + CASE WHEN s_q2 THEN + (COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arv,0)) + - (COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)) + END, + CASE WHEN s_q2 THEN tb2.tq3 END, + CASE WHEN s_q2 THEN tb2.tda END, CASE WHEN s_q2 THEN tb2.tdp END, CASE WHEN s_q2 THEN tb2.terf END, + NULL::VARCHAR, -- transfer_next_comment (text, no rollup) + CASE WHEN s_q2 THEN tb2.tq4 END, + NULL::VARCHAR, -- far comment + CASE WHEN s_q2 THEN tb2.te END, + CASE WHEN s_q2 THEN + COALESCE(tb2.tda,0)+COALESCE(tb2.tdp,0)+COALESCE(tb2.terf,0) + +COALESCE(tb2.tq4,0)+COALESCE(tb2.te,0) + END, + NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- target_change/base_correction/comment + NULL::NUMERIC, NULL::NUMERIC, -- revision_inc/revision_seq + NULL::VARCHAR, + CASE WHEN s_q3 THEN + COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0) + END, + CASE WHEN s_q3 THEN tb3.ac END, CASE WHEN s_q3 THEN tb3.as_v END, CASE WHEN s_q3 THEN tb3.arv END, + NULL::VARCHAR, + CASE WHEN s_q3 THEN + COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arv,0) + END, + NULL::DATE, + CASE WHEN s_q3 THEN tb3.pa END, + NULL::VARCHAR, -- pay_comment + NULL::VARCHAR, -- pay_act + CASE WHEN s_q3 THEN tb3.bk END, -- booking (rollup) + CASE WHEN s_q3 THEN tb3.m1 END, CASE WHEN s_q3 THEN tb3.m2 END, CASE WHEN s_q3 THEN tb3.m3 END, + CASE WHEN s_q3 THEN COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0) END, + CASE WHEN s_q3 THEN + (COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arv,0)) + - COALESCE(tb3.bk,0) + END, + CASE WHEN s_q3 THEN + (COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arv,0)) + - (COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)) + END, + CASE WHEN s_q3 THEN tb3.tq4 END, + CASE WHEN s_q3 THEN tb3.tda END, CASE WHEN s_q3 THEN tb3.tdp END, CASE WHEN s_q3 THEN tb3.terf END, + NULL::VARCHAR, -- transfer_next_comment (text, no rollup) + CASE WHEN s_q3 THEN tb3.te END, + CASE WHEN s_q3 THEN + COALESCE(tb3.tda,0)+COALESCE(tb3.tdp,0)+COALESCE(tb3.terf,0) + +COALESCE(tb3.te,0) + END, + NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- target_change/base_correction/comment + NULL::NUMERIC, NULL::NUMERIC, -- revision_inc/revision_seq + NULL::VARCHAR, + CASE WHEN s_q4 THEN + COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0) + END, + CASE WHEN s_q4 THEN tb4.ac END, CASE WHEN s_q4 THEN tb4.as_v END, CASE WHEN s_q4 THEN tb4.arv END, + NULL::VARCHAR, + CASE WHEN s_q4 THEN + COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arv,0) + END, + NULL::DATE, + CASE WHEN s_q4 THEN tb4.pa END, + NULL::VARCHAR, -- pay_comment + NULL::VARCHAR, -- pay_act + CASE WHEN s_q4 THEN tb4.bk END, -- booking (rollup) + CASE WHEN s_q4 THEN tb4.m1 END, CASE WHEN s_q4 THEN tb4.m2 END, CASE WHEN s_q4 THEN tb4.m3 END, + CASE WHEN s_q4 THEN tb4.spod END, + CASE WHEN s_q4 THEN COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0) END, + CASE WHEN s_q4 THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arv,0)) + - COALESCE(tb4.bk,0) + END, + CASE WHEN s_q4 THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arv,0)) + - (COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)) + END, + CASE WHEN s_q4 THEN tb4.te END, + CASE WHEN s_q4 THEN tb4.te END, + CASE WHEN s_tot THEN COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0) + +COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0) + +COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0) + +COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0) END, + CASE WHEN s_tot THEN COALESCE(tb1.pa,0)+COALESCE(tb2.pa,0)+COALESCE(tb3.pa,0)+COALESCE(tb4.pa,0) END, + CASE WHEN s_tot THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0) + +COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0) + +COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arv,0)) + - (COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)) + END, + + NULL::VARCHAR, -- internal_order + NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, -- contract_summary + NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, + NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, -- allocation + NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, -- collegial + NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, -- ckk + NULL::VARCHAR, NULL::VARCHAR, + NULL::VARCHAR, CASE WHEN s_cd THEN ta.bkc1 END, CASE WHEN s_cd THEN ta.bkc2 END, + CASE WHEN s_cd THEN ta.bkc3 END, CASE WHEN s_cd THEN ta.bkc4 END, + NULL::VARCHAR, NULL::NUMERIC, NULL::NUMERIC, + NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, -- q1 adj_rf/pho/prf + NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, -- q2 + NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, -- q3 + NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, -- q4 + + t.path AS _sort + FROM tw t + LEFT JOIN tw_agg ta ON ta.id = t.id + LEFT JOIN tw_aq1 tb1 ON tb1.id = t.id + LEFT JOIN tw_aq2 tb2 ON tb2.id = t.id + LEFT JOIN tw_aq3 tb3 ON tb3.id = t.id + LEFT JOIN tw_aq4 tb4 ON tb4.id = t.id + ) sub + ORDER BY sub._sort; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION v3.v_form2_sheet_jsonb(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[]) + RETURNS TABLE(row_type character varying, depth integer, line_id integer, header jsonb, plan_data jsonb, seq_dfip_data jsonb, seq_ssp_data jsonb, approved_data jsonb, contract_data jsonb, booking_data jsonb, contract_summary jsonb, allocation_data jsonb, collegial_data jsonb, ckk_data jsonb, q1_data jsonb, q2_data jsonb, q3_data jsonb, q4_data jsonb, totals_data jsonb, _sort_path integer[]) + LANGUAGE sql + STABLE +AS $function$ + WITH flags AS ( + SELECT + (p_sections IS NULL OR 'plan' = ANY(p_sections)) AS s_plan, + (p_sections IS NULL OR 'seq_dfip' = ANY(p_sections)) AS s_seq_d, + (p_sections IS NULL OR 'seq_ssp' = ANY(p_sections)) AS s_seq_s, + (p_sections IS NULL OR 'approved' = ANY(p_sections)) AS s_appr, + (p_sections IS NULL OR 'contract' = ANY(p_sections)) AS s_cd, + (p_sections IS NULL OR 'booking' = ANY(p_sections)) AS s_book, + (p_sections IS NULL OR 'contract_summary' = ANY(p_sections)) AS s_cs, + (p_sections IS NULL OR 'allocation' = ANY(p_sections)) AS s_al, + (p_sections IS NULL OR 'collegial' = ANY(p_sections)) AS s_ca, + (p_sections IS NULL OR 'ckk' = ANY(p_sections)) AS s_ckk, + (p_sections IS NULL OR 'q1' = ANY(p_sections)) AS s_q1, + (p_sections IS NULL OR 'q2' = ANY(p_sections)) AS s_q2, + (p_sections IS NULL OR 'q3' = ANY(p_sections)) AS s_q3, + (p_sections IS NULL OR 'q4' = ANY(p_sections)) AS s_q4, + (p_sections IS NULL OR 'totals' = ANY(p_sections)) AS s_tot + ) + SELECT + s.row_type, s.depth, s.line_id, + + jsonb_build_object( + 'section_code', s.col_section_code, + 'item_id', s.col_item_id, + 'num_group', s.col_num_group_id, + 'name', s.col_name, + 'vsp_id', s.col_vsp_id, + 'vsp_address', s.col_vsp_address, + 'internal_order', s.col_internal_order, + 'expense_item_id', CASE WHEN s.row_type = 'INPUT' + THEN s._sort_path[array_upper(s._sort_path,1)-1] + ELSE s._sort_path[array_upper(s._sort_path,1)] END + ) AS header, + + CASE WHEN f.s_plan THEN jsonb_build_object( + 'q1', s.col_plan_q1, 'q2', s.col_plan_q2, + 'q3', s.col_plan_q3, 'q4', s.col_plan_q4, + 'year', s.col_plan_year, 'comment', s.col_plan_comment + ) END AS plan_data, + + CASE WHEN f.s_seq_d THEN jsonb_build_object( + 'q1', s.col_seq_dfip_q1, 'q2', s.col_seq_dfip_q2, + 'q3', s.col_seq_dfip_q3, 'q4', s.col_seq_dfip_q4, + 'year', s.col_seq_dfip_year, + 'justification', s.col_seq_dfip_just + ) END AS seq_dfip_data, + + CASE WHEN f.s_seq_s THEN jsonb_build_object( + 'q1', s.col_seq_ssp_q1, 'q2', s.col_seq_ssp_q2, + 'q3', s.col_seq_ssp_q3, 'q4', s.col_seq_ssp_q4, + 'year', s.col_seq_ssp_year, + 'justification', s.col_seq_ssp_just + ) END AS seq_ssp_data, + + CASE WHEN f.s_appr THEN jsonb_build_object( + 'q1', s.col_appr_q1, 'q2', s.col_appr_q2, + 'q3', s.col_appr_q3, 'q4', s.col_appr_q4, + 'year', s.col_appr_year + ) END AS approved_data, + + CASE WHEN f.s_cd THEN jsonb_build_object( + 'counterparty', s.col_cd_counterparty, + 'reference', s.col_cd_reference, + 'addenda', s.col_cd_addenda, + 'date', TO_CHAR(s.col_cd_date, 'DD.MM.YYYY'), + 'subject', s.col_cd_subject, + 'currency', s.col_cd_currency, + 'ceiling', s.col_cd_ceiling, + 'q1', s.col_cd_exp_q1, + 'q2', s.col_cd_exp_q2, + 'q3', s.col_cd_exp_q3, + 'q4', s.col_cd_exp_q4, + 'rf_schedule', s.col_cd_rf_sched, + 'vat_rate', s.col_cd_vat_rate, + 'exchange_rate', s.col_cd_exchange_rate, + 'amount_foreign', s.col_cd_amount_foreign, + 'deadline', s.col_cd_deadline, + 'scheme', s.col_cd_scheme, + 'act', s.col_cd_act, + 'comment', s.col_cd_comment + ) END AS contract_data, + + CASE WHEN f.s_book THEN jsonb_build_object( + 'y2026', jsonb_build_object('q1', s.col_book_q1, 'q2', s.col_book_q2, 'q3', s.col_book_q3, 'q4', s.col_book_q4), + 'y2027', jsonb_build_object('q1', s.col_book_next_q1, 'q2', s.col_book_next_q2, 'q3', s.col_book_next_q3, 'q4', s.col_book_next_q4) + ) END AS booking_data, + + CASE WHEN f.s_cs THEN jsonb_build_object( + 'total', s.col_cs_total, + 'counterparty', s.col_cs_counterparty, + 'deadline', s.col_cs_deadline, + 'comment', s.col_cs_comment, + 'future_y1', s.col_cs_future_y1, + 'future_y2', s.col_cs_future_y2, + 'other_ssp', s.col_cs_other_ssp + ) END AS contract_summary, + + CASE WHEN f.s_al THEN jsonb_build_object( + 'contract_ref', s.col_al_contract_ref, + 'purpose', s.col_al_purpose, + 'order', s.col_al_order, + 'property', s.col_al_property + ) END AS allocation_data, + + CASE WHEN f.s_ca THEN jsonb_build_object( + 'approved', s.col_ca_approved, + 'protocol', s.col_ca_protocol, + 'note', s.col_ca_note + ) END AS collegial_data, + + CASE WHEN f.s_ckk THEN jsonb_build_object( + 'ceiling', s.col_ckk_ceiling, + 'rf_schedule', s.col_ckk_rf_sched, + 'deadline', s.col_ckk_deadline, + 'proc_plan', s.col_ckk_proc_plan, + 'proc_method', s.col_ckk_proc_meth, + 'comment', s.col_ckk_comment + ) END AS ckk_data, + + CASE WHEN f.s_q1 THEN jsonb_build_object( + 'adj_current', s.col_q1_adj_current, + 'adj_ssp', s.col_q1_adj_ssp, + 'adj_rf', s.col_q1_adj_rf, + 'adj_reserve', s.col_q1_adj_reserve, + 'adj_comment', s.col_q1_adj_comment, + 'corrected_plan', s.col_q1_corrected_plan, + 'pay_date', TO_CHAR(s.col_q1_pay_date, 'DD.MM.YYYY'), + 'pay_amount', s.col_q1_pay_amount, + 'pay_ho', s.col_q1_pay_ho, + 'pay_rf', s.col_q1_pay_rf, + 'pay_comment', s.col_q1_pay_comment, + 'pay_act', s.col_q1_pay_act, + 'booking', s.col_q1_booking, + 'actual_m1', s.col_q1_actual_m1, + 'actual_m2', s.col_q1_actual_m2, + 'actual_m3', s.col_q1_actual_m3, + 'actual_quarter', s.col_q1_actual_quarter, + 'residual_after_booking', s.col_q1_residual_after_booking, + 'residual_after_actual', s.col_q1_residual_after_actual, + 'transfer_q2', s.col_q1_transfer_q2, + 'transfer_q2_delay_acts', s.col_q1_transfer_q2_delay_acts, + 'transfer_q2_delay_procurement', s.col_q1_transfer_q2_delay_procurement, + 'transfer_q2_economy_rf', s.col_q1_transfer_q2_economy_rf, + 'transfer_next_comment', s.col_q1_transfer_next_comment, + 'transfer_q3', s.col_q1_transfer_q3, + 'transfer_q4', s.col_q1_transfer_q4, + 'transfer_far_comment', s.col_q1_transfer_far_comment, + 'transfer_econ', s.col_q1_transfer_econ, + 'total', s.col_q1_total + ) END AS q1_data, + + CASE WHEN f.s_q2 THEN jsonb_build_object( + 'target_change', s.col_q2_target_change, + 'base_correction', s.col_q2_base_correction, + 'base_correction_comment', s.col_q2_base_correction_comment, + 'revision_inc', s.col_q2_revision_inc, + 'revision_seq', s.col_q2_revision_seq, + 'revision_comment', s.col_q2_revision_comment, + 'new_plan', s.col_q2_new_plan, + 'adj_current', s.col_q2_adj_current, + 'adj_ssp', s.col_q2_adj_ssp, + 'adj_rf', s.col_q2_adj_rf, + 'adj_reserve', s.col_q2_adj_reserve, + 'adj_comment', s.col_q2_adj_comment, + 'corrected_plan', s.col_q2_corrected_plan, + 'pay_date', TO_CHAR(s.col_q2_pay_date, 'DD.MM.YYYY'), + 'pay_amount', s.col_q2_pay_amount, + 'pay_ho', s.col_q2_pay_ho, + 'pay_rf', s.col_q2_pay_rf, + 'pay_comment', s.col_q2_pay_comment, + 'pay_act', s.col_q2_pay_act, + 'booking', s.col_q2_booking, + 'actual_m1', s.col_q2_actual_m1, + 'actual_m2', s.col_q2_actual_m2, + 'actual_m3', s.col_q2_actual_m3, + 'actual_quarter', s.col_q2_actual_quarter, + 'residual_after_booking', s.col_q2_residual_after_booking, + 'residual_after_actual', s.col_q2_residual_after_actual, + 'transfer_q3', s.col_q2_transfer_q3, + 'transfer_q3_delay_acts', s.col_q2_transfer_q3_delay_acts, + 'transfer_q3_delay_procurement', s.col_q2_transfer_q3_delay_procurement, + 'transfer_q3_economy_rf', s.col_q2_transfer_q3_economy_rf, + 'transfer_next_comment', s.col_q2_transfer_next_comment, + 'transfer_q4', s.col_q2_transfer_q4, + 'transfer_far_comment', s.col_q2_transfer_far_comment, + 'transfer_econ', s.col_q2_transfer_econ, + 'total', s.col_q2_total + ) END AS q2_data, + + CASE WHEN f.s_q3 THEN jsonb_build_object( + 'target_change', s.col_q3_target_change, + 'base_correction', s.col_q3_base_correction, + 'base_correction_comment', s.col_q3_base_correction_comment, + 'revision_inc', s.col_q3_revision_inc, + 'revision_seq', s.col_q3_revision_seq, + 'revision_comment', s.col_q3_revision_comment, + 'new_plan', s.col_q3_new_plan, + 'adj_current', s.col_q3_adj_current, + 'adj_ssp', s.col_q3_adj_ssp, + 'adj_rf', s.col_q3_adj_rf, + 'adj_reserve', s.col_q3_adj_reserve, + 'adj_comment', s.col_q3_adj_comment, + 'corrected_plan', s.col_q3_corrected_plan, + 'pay_date', TO_CHAR(s.col_q3_pay_date, 'DD.MM.YYYY'), + 'pay_amount', s.col_q3_pay_amount, + 'pay_ho', s.col_q3_pay_ho, + 'pay_rf', s.col_q3_pay_rf, + 'pay_comment', s.col_q3_pay_comment, + 'pay_act', s.col_q3_pay_act, + 'booking', s.col_q3_booking, + 'actual_m1', s.col_q3_actual_m1, + 'actual_m2', s.col_q3_actual_m2, + 'actual_m3', s.col_q3_actual_m3, + 'actual_quarter', s.col_q3_actual_quarter, + 'residual_after_booking', s.col_q3_residual_after_booking, + 'residual_after_actual', s.col_q3_residual_after_actual, + 'transfer_q4', s.col_q3_transfer_q4, + 'transfer_q4_delay_acts', s.col_q3_transfer_q4_delay_acts, + 'transfer_q4_delay_procurement', s.col_q3_transfer_q4_delay_procurement, + 'transfer_q4_economy_rf', s.col_q3_transfer_q4_economy_rf, + 'transfer_next_comment', s.col_q3_transfer_next_comment, + 'transfer_econ', s.col_q3_transfer_econ, + 'total', s.col_q3_total + ) END AS q3_data, + + CASE WHEN f.s_q4 THEN jsonb_build_object( + 'target_change', s.col_q4_target_change, + 'base_correction', s.col_q4_base_correction, + 'base_correction_comment', s.col_q4_base_correction_comment, + 'revision_inc', s.col_q4_revision_inc, + 'revision_seq', s.col_q4_revision_seq, + 'revision_comment', s.col_q4_revision_comment, + 'new_plan', s.col_q4_new_plan, + 'adj_current', s.col_q4_adj_current, + 'adj_ssp', s.col_q4_adj_ssp, + 'adj_rf', s.col_q4_adj_rf, + 'adj_reserve', s.col_q4_adj_reserve, + 'adj_comment', s.col_q4_adj_comment, + 'corrected_plan', s.col_q4_corrected_plan, + 'pay_date', TO_CHAR(s.col_q4_pay_date, 'DD.MM.YYYY'), + 'pay_amount', s.col_q4_pay_amount, + 'pay_ho', s.col_q4_pay_ho, + 'pay_rf', s.col_q4_pay_rf, + 'pay_comment', s.col_q4_pay_comment, + 'pay_act', s.col_q4_pay_act, + 'booking', s.col_q4_booking, + 'actual_m1', s.col_q4_actual_m1, + 'actual_m2', s.col_q4_actual_m2, + 'actual_m3', s.col_q4_actual_m3, + 'actual_spod', s.col_q4_actual_spod, + 'actual_quarter', s.col_q4_actual_quarter, + 'residual_after_booking', s.col_q4_residual_after_booking, + 'residual_after_actual', s.col_q4_residual_after_actual, + 'transfer_econ', s.col_q4_transfer_econ, + 'total', s.col_q4_total + ) END AS q4_data, + + CASE WHEN f.s_tot THEN jsonb_build_object( + 'fact_year', s.col_fact_year, + 'pay_year', s.col_pay_year, + 'economy_year', s.col_economy_year + ) END AS totals_data, + + s._sort_path + FROM v3.v_form2_sheet_sections(p_form_id, p_sheet, p_sections) s + CROSS JOIN flags f + ORDER BY s._sort_path; +$function$ +; + +CREATE OR REPLACE FUNCTION v3.v_form2_view(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[]) + RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) + LANGUAGE plpgsql + STABLE +AS $function$ +BEGIN + + + IF p_sheet IN ('AHR','CAP','OPER') THEN + RETURN QUERY + SELECT + j.row_type, + j.depth, + ROW_NUMBER() OVER (ORDER BY j._sort_path) AS sort_order, + jsonb_build_object( + 'line_id', j.line_id, + 'header', j.header, + 'plan', j.plan_data, + 'seq_dfip', j.seq_dfip_data, + 'seq_ssp', j.seq_ssp_data, + 'approved', j.approved_data, + 'contract', j.contract_data, + 'booking', j.booking_data, + 'q1', j.q1_data, + 'q2', j.q2_data, + 'q3', j.q3_data, + 'q4', j.q4_data, + 'totals', j.totals_data + ) AS data + FROM v3.v_form2_sheet_jsonb(p_form_id, p_sheet, p_sections) j + ORDER BY j._sort_path; + RETURN; + END IF; + + + + + + + + + + + + + IF p_sheet = 'AHR_LIMIT' THEN + RETURN QUERY + WITH RECURSIVE tree AS ( + SELECT id AS root_id, id AS desc_id FROM v3.limit_template + UNION + SELECT t.root_id, child.id + FROM tree t + JOIN v3.limit_template child ON child.parent_id = t.desc_id + ), + leaf_amt AS ( + + SELECT lt.id, + fl.qty_q1, fl.qty_q2, fl.qty_q3, fl.qty_q4, + v3._form2_ahr_limit_amount(lt.name, lt.unit, lt.limit_with_vat, fl.qty_q1)AS amount_q1, + v3._form2_ahr_limit_amount(lt.name, lt.unit, lt.limit_with_vat, fl.qty_q2)AS amount_q2, + v3._form2_ahr_limit_amount(lt.name, lt.unit, lt.limit_with_vat, fl.qty_q3)AS amount_q3, + v3._form2_ahr_limit_amount(lt.name, lt.unit, lt.limit_with_vat, fl.qty_q4)AS amount_q4 + FROM v3.limit_template lt + LEFT JOIN v3.form_limit fl + ON fl.template_id = lt.id AND fl.budget_form_id = p_form_id + WHERE lt.row_type = 'LEAF' + ), + agg AS ( + SELECT t.root_id, + SUM(la.qty_q1) AS qty_q1, SUM(la.amount_q1) AS amount_q1, + SUM(la.qty_q2) AS qty_q2, SUM(la.amount_q2) AS amount_q2, + SUM(la.qty_q3) AS qty_q3, SUM(la.amount_q3) AS amount_q3, + SUM(la.qty_q4) AS qty_q4, SUM(la.amount_q4) AS amount_q4 + FROM tree t + JOIN leaf_amt la ON la.id = t.desc_id + GROUP BY t.root_id + ) + SELECT + + + CASE lt.row_type WHEN 'SECTION' THEN 'ROOT' + WHEN 'GROUP' THEN 'GROUP' + WHEN 'LEAF' THEN 'INPUT' END::VARCHAR, + CASE lt.row_type WHEN 'SECTION' THEN 0 WHEN 'GROUP' THEN 1 ELSE 2 END, + lt.sort_order::BIGINT, + jsonb_build_object( + 'id', lt.id, + 'section_no', lt.section_no, + 'expense_item_code',lt.expense_item_code, + 'name', lt.name, + 'unit', lt.unit, + 'limit_with_vat', lt.limit_with_vat, + 'limit_without_vat',lt.limit_without_vat, + 'qty_q1', a.qty_q1, 'amount_q1', a.amount_q1, + 'qty_q2', a.qty_q2, 'amount_q2', a.amount_q2, + 'qty_q3', a.qty_q3, 'amount_q3', a.amount_q3, + 'qty_q4', a.qty_q4, 'amount_q4', a.amount_q4, + 'comment', fl.comment + ) + FROM v3.limit_template lt + LEFT JOIN agg a ON a.root_id = lt.id + LEFT JOIN v3.form_limit fl + ON fl.template_id = lt.id AND fl.budget_form_id = p_form_id + ORDER BY lt.sort_order; + RETURN; + END IF; + + + IF p_sheet = 'AHR_RENT' THEN + RETURN QUERY + SELECT + 'INPUT'::VARCHAR, + 0, + ROW_NUMBER() OVER (ORDER BY rd.id) AS sort_order, + jsonb_build_object( + 'id', rd.id, + 'line_id', rd.line_id, + 'vsp_id', rd.vsp_id, + 'address', v.address, + 'object_type', v.format, + 'rented_area', v.total_area, + 'contract_number', rd.contract_number, + 'contract_end_date',rd.contract_end_date, + 'plan', jsonb_build_object('q1', rd.plan_q1, 'q2', rd.plan_q2, 'q3', rd.plan_q3, 'q4', rd.plan_q4, + 'year', COALESCE(rd.plan_q1,0)+COALESCE(rd.plan_q2,0)+COALESCE(rd.plan_q3,0)+COALESCE(rd.plan_q4,0)), + 'fact_q1', jsonb_build_object('jan', rd.actual_jan, 'feb', rd.actual_feb, 'mar', rd.actual_mar, + 'total', COALESCE(rd.actual_jan,0)+COALESCE(rd.actual_feb,0)+COALESCE(rd.actual_mar,0)), + 'fact_q2', jsonb_build_object('apr', rd.actual_apr, 'may', rd.actual_may, 'jun', rd.actual_jun, + 'total', COALESCE(rd.actual_apr,0)+COALESCE(rd.actual_may,0)+COALESCE(rd.actual_jun,0)), + 'fact_q3', jsonb_build_object('jul', rd.actual_jul, 'aug', rd.actual_aug, 'sep', rd.actual_sep, + 'total', COALESCE(rd.actual_jul,0)+COALESCE(rd.actual_aug,0)+COALESCE(rd.actual_sep,0)), + 'fact_q4', jsonb_build_object('oct', rd.actual_oct, 'nov', rd.actual_nov, 'dec', rd.actual_dec, + 'total', COALESCE(rd.actual_oct,0)+COALESCE(rd.actual_nov,0)+COALESCE(rd.actual_dec,0)), + 'comment', rd.comment + ) + FROM v3.rent_detail rd + JOIN v3.budget_line bl ON bl.id = rd.line_id + JOIN v3.vsp v ON v.id = rd.vsp_id + WHERE bl.budget_form_id = p_form_id + ORDER BY rd.id; + RETURN; + END IF; + + + IF p_sheet = 'AHR_UTILITY' THEN + RETURN QUERY + SELECT + 'INPUT'::VARCHAR, + 0, + ROW_NUMBER() OVER (ORDER BY ud.id) AS sort_order, + jsonb_build_object( + 'id', ud.id, + 'line_id', ud.line_id, + 'vsp_id', ud.vsp_id, + 'address', v.address, + 'object_type', v.format, + 'object_area', v.total_area, + 'contract_number', ud.contract_number, + 'contract_end_date',ud.contract_end_date, + 'plan', jsonb_build_object('q1', ud.plan_q1, 'q2', ud.plan_q2, 'q3', ud.plan_q3, 'q4', ud.plan_q4, + 'year', COALESCE(ud.plan_q1,0)+COALESCE(ud.plan_q2,0)+COALESCE(ud.plan_q3,0)+COALESCE(ud.plan_q4,0)), + 'fact_q1', jsonb_build_object('jan', ud.actual_jan, 'feb', ud.actual_feb, 'mar', ud.actual_mar, + 'total', COALESCE(ud.actual_jan,0)+COALESCE(ud.actual_feb,0)+COALESCE(ud.actual_mar,0)), + 'fact_q2', jsonb_build_object('apr', ud.actual_apr, 'may', ud.actual_may, 'jun', ud.actual_jun, + 'total', COALESCE(ud.actual_apr,0)+COALESCE(ud.actual_may,0)+COALESCE(ud.actual_jun,0)), + 'fact_q3', jsonb_build_object('jul', ud.actual_jul, 'aug', ud.actual_aug, 'sep', ud.actual_sep, + 'total', COALESCE(ud.actual_jul,0)+COALESCE(ud.actual_aug,0)+COALESCE(ud.actual_sep,0)), + 'fact_q4', jsonb_build_object('oct', ud.actual_oct, 'nov', ud.actual_nov, 'dec', ud.actual_dec, + 'total', COALESCE(ud.actual_oct,0)+COALESCE(ud.actual_nov,0)+COALESCE(ud.actual_dec,0)), + 'comment', ud.comment + ) + FROM v3.utility_detail ud + JOIN v3.budget_line bl ON bl.id = ud.line_id + JOIN v3.vsp v ON v.id = ud.vsp_id + WHERE bl.budget_form_id = p_form_id + ORDER BY ud.id; + RETURN; + END IF; + + + IF p_sheet = 'AHR_SECURITY' THEN + RETURN QUERY + SELECT + 'INPUT'::VARCHAR, + 0, + ROW_NUMBER() OVER (ORDER BY sd.id) AS sort_order, + jsonb_build_object( + 'id', sd.id, + 'line_id', sd.line_id, + 'vsp_id', sd.vsp_id, + 'address', v.address, + 'object_type', v.format, + 'object_area', v.total_area, + 'contract_number', sd.contract_number, + 'contract_end_date',sd.contract_end_date, + 'plan', jsonb_build_object('q1', sd.plan_q1, 'q2', sd.plan_q2, 'q3', sd.plan_q3, 'q4', sd.plan_q4, + 'year', COALESCE(sd.plan_q1,0)+COALESCE(sd.plan_q2,0)+COALESCE(sd.plan_q3,0)+COALESCE(sd.plan_q4,0)), + 'fact_q1', jsonb_build_object('jan', sd.actual_jan, 'feb', sd.actual_feb, 'mar', sd.actual_mar, + 'total', COALESCE(sd.actual_jan,0)+COALESCE(sd.actual_feb,0)+COALESCE(sd.actual_mar,0)), + 'fact_q2', jsonb_build_object('apr', sd.actual_apr, 'may', sd.actual_may, 'jun', sd.actual_jun, + 'total', COALESCE(sd.actual_apr,0)+COALESCE(sd.actual_may,0)+COALESCE(sd.actual_jun,0)), + 'fact_q3', jsonb_build_object('jul', sd.actual_jul, 'aug', sd.actual_aug, 'sep', sd.actual_sep, + 'total', COALESCE(sd.actual_jul,0)+COALESCE(sd.actual_aug,0)+COALESCE(sd.actual_sep,0)), + 'fact_q4', jsonb_build_object('oct', sd.actual_oct, 'nov', sd.actual_nov, 'dec', sd.actual_dec, + 'total', COALESCE(sd.actual_oct,0)+COALESCE(sd.actual_nov,0)+COALESCE(sd.actual_dec,0)), + 'comment', sd.comment + ) + FROM v3.security_detail sd + JOIN v3.budget_line bl ON bl.id = sd.line_id + JOIN v3.vsp v ON v.id = sd.vsp_id + WHERE bl.budget_form_id = p_form_id + ORDER BY sd.id; + RETURN; + END IF; + + + + + + + + + + IF p_sheet = 'OTCH9F' THEN + RETURN QUERY + WITH base AS ( + SELECT far.id, far.budget_form_id, far.expense_item_id, + far.equipment_name, far.month, + far.acquired_qty_604, far.acquired_amt_604, + far.disposed_qty_604, far.disposed_amt_604, + far.acquired_qty_60415, far.acquired_amt_60415, + far.transferred_qty_60415, far.transferred_amt_60415, + far.go_balance_only_amt, + + FIRST_VALUE(COALESCE(far.opening_qty_604,0)) + OVER w AS base_qty_604, + FIRST_VALUE(COALESCE(far.opening_amt_604,0)) + OVER w AS base_amt_604, + FIRST_VALUE(COALESCE(far.opening_qty_60415,0)) + OVER w AS base_qty_60415, + FIRST_VALUE(COALESCE(far.opening_amt_60415,0)) + OVER w AS base_amt_60415, + + COALESCE(SUM(COALESCE(far.acquired_qty_604,0) - COALESCE(far.disposed_qty_604,0)) + OVER w_prev, 0) AS cum_dq_604, + COALESCE(SUM(COALESCE(far.acquired_amt_604,0) - COALESCE(far.disposed_amt_604,0)) + OVER w_prev, 0) AS cum_da_604, + COALESCE(SUM(COALESCE(far.acquired_qty_60415,0) - COALESCE(far.transferred_qty_60415,0)) + OVER w_prev, 0) AS cum_dq_60415, + COALESCE(SUM(COALESCE(far.acquired_amt_60415,0) - COALESCE(far.transferred_amt_60415,0)) + OVER w_prev, 0) AS cum_da_60415 + FROM v3.fixed_asset_report far + WHERE far.budget_form_id = p_form_id + WINDOW w AS (PARTITION BY far.budget_form_id, far.expense_item_id, far.equipment_name + ORDER BY far.month), + w_prev AS (PARTITION BY far.budget_form_id, far.expense_item_id, far.equipment_name + ORDER BY far.month + ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) + ), + eff AS ( + SELECT b.*, + (b.base_qty_604 + b.cum_dq_604) AS opening_qty_604_eff, + (b.base_amt_604 + b.cum_da_604) AS opening_amt_604_eff, + (b.base_qty_60415 + b.cum_dq_60415) AS opening_qty_60415_eff, + (b.base_amt_60415 + b.cum_da_60415) AS opening_amt_60415_eff + FROM base b + ) + SELECT + 'INPUT'::VARCHAR, + 0, + ROW_NUMBER() OVER (ORDER BY eff.expense_item_id, eff.month) AS sort_order, + jsonb_build_object( + 'id', eff.id, + 'expense_item_id', ei.id, + 'item_id', ei.item_id, + 'section_code', ei.section_code, + 'equipment_name', eff.equipment_name, + 'month', eff.month, + 'b_604', jsonb_build_object( + 'opening_qty', eff.opening_qty_604_eff, + 'opening_amt', eff.opening_amt_604_eff, + 'acquired_qty', eff.acquired_qty_604, + 'acquired_amt', eff.acquired_amt_604, + 'disposed_qty', eff.disposed_qty_604, + 'disposed_amt', eff.disposed_amt_604 + ), + 'b_60415', jsonb_build_object( + 'opening_qty', eff.opening_qty_60415_eff, + 'opening_amt', eff.opening_amt_60415_eff, + 'acquired_qty', eff.acquired_qty_60415, + 'acquired_amt', eff.acquired_amt_60415, + 'transferred_qty',eff.transferred_qty_60415, + 'transferred_amt',eff.transferred_amt_60415 + ), + 'totals', jsonb_build_object( + + 'acquired_total', COALESCE(eff.acquired_amt_604,0)+COALESCE(eff.acquired_amt_60415,0) + +COALESCE(eff.go_balance_only_amt,0)-COALESCE(eff.transferred_amt_60415,0), + 'go_balance_only',eff.go_balance_only_amt, + + 'disposed_total', COALESCE(eff.disposed_amt_604,0), + + 'balance_qty', (eff.opening_qty_604_eff + COALESCE(eff.acquired_qty_604,0) - COALESCE(eff.disposed_qty_604,0)) + + (eff.opening_qty_60415_eff + COALESCE(eff.acquired_qty_60415,0) - COALESCE(eff.transferred_qty_60415,0)), + 'balance_amt', (eff.opening_amt_604_eff + COALESCE(eff.acquired_amt_604,0) - COALESCE(eff.disposed_amt_604,0)) + + (eff.opening_amt_60415_eff + COALESCE(eff.acquired_amt_60415,0) - COALESCE(eff.transferred_amt_60415,0)) + ) + ) + FROM eff + JOIN v3.expense_item ei ON ei.id = eff.expense_item_id + ORDER BY eff.expense_item_id, eff.month; + RETURN; + END IF; + + + IF p_sheet = 'SMETA' THEN + RETURN QUERY + SELECT + sm.row_type, + sm.depth, + ROW_NUMBER() OVER (ORDER BY sm.section_code) AS sort_order, + jsonb_build_object( + 'section_code', sm.section_code, + 'name', sm.name, + 'plan', jsonb_build_object( + 'support', jsonb_build_object('q1', sm.plan_supp_q1, 'q2', sm.plan_supp_q2, 'q3', sm.plan_supp_q3, 'q4', sm.plan_supp_q4, 'year', sm.plan_supp_year), + 'development', jsonb_build_object('q1', sm.plan_dev_q1, 'q2', sm.plan_dev_q2, 'q3', sm.plan_dev_q3, 'q4', sm.plan_dev_q4, 'year', sm.plan_dev_year) + ), + 'approved', jsonb_build_object( + 'support', jsonb_build_object('q1', sm.appr_supp_q1, 'q2', sm.appr_supp_q2, 'q3', sm.appr_supp_q3, 'q4', sm.appr_supp_q4, 'year', sm.appr_supp_year), + 'development', jsonb_build_object('q1', sm.appr_dev_q1, 'q2', sm.appr_dev_q2, 'q3', sm.appr_dev_q3, 'q4', sm.appr_dev_q4, 'year', sm.appr_dev_year) + ), + 'fact', jsonb_build_object( + 'support', jsonb_build_object('q1', sm.fact_supp_q1, 'q2', sm.fact_supp_q2, 'q3', sm.fact_supp_q3, 'q4', sm.fact_supp_q4, 'year', sm.fact_supp_year), + 'development', jsonb_build_object('q1', sm.fact_dev_q1, 'q2', sm.fact_dev_q2, 'q3', sm.fact_dev_q3, 'q4', sm.fact_dev_q4, 'year', sm.fact_dev_year) + ), + + + 'corrected', jsonb_build_object( + 'support', jsonb_build_object('q2', sm.corr_supp_q2, 'q3', sm.corr_supp_q3, 'q4', sm.corr_supp_q4), + 'development', jsonb_build_object('q2', sm.corr_dev_q2, 'q3', sm.corr_dev_q3, 'q4', sm.corr_dev_q4) + ) + ) + FROM v3.v_form2_smeta(p_form_id) sm + ORDER BY sm.section_code; + RETURN; + END IF; + + RAISE EXCEPTION 'Unknown sheet: %', p_sheet + USING HINT = 'Use AHR / CAP / OPER / AHR_LIMIT / AHR_RENT / AHR_UTILITY / AHR_SECURITY / OTCH9F / SMETA'; + +END; +$function$ +; + +CREATE OR REPLACE FUNCTION v3.v_form1_sheet_sections(p_form_id integer, p_sheet character varying, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[]) + RETURNS TABLE(row_type character varying, depth integer, line_id integer, col_b_year integer, col_c_section character varying, col_d_item_id character varying, col_e_num_group character varying, col_f_name character varying, col_g_internal_order character varying, col_h_plan_q1 numeric, col_i_plan_q2 numeric, col_j_plan_q3 numeric, col_k_plan_q4 numeric, col_l_plan_year numeric, col_m_plan_comment character varying, col_n_cs_total numeric, col_o_cs_counterparty character varying, col_p_cs_deadline character varying, col_q_cs_comment character varying, col_r_cs_future_y1 numeric, col_s_cs_future_y2 numeric, col_t_cs_other_ssp numeric, col_v_al_order character varying, col_w_al_property character varying, col_y_seq_q1 numeric, col_z_seq_q2 numeric, col_aa_seq_q3 numeric, col_ab_seq_q4 numeric, col_ac_seq_year numeric, col_ad_seq_justification character varying, col_af_res_q1 numeric, col_ag_res_q2 numeric, col_ah_res_q3 numeric, col_ai_res_q4 numeric, col_aj_res_year numeric, col_ak_res_justification character varying, col_am_approved_q1 numeric, col_an_approved_q2 numeric, col_ao_approved_q3 numeric, col_ap_approved_q4 numeric, col_aq_approved_year numeric, col_as_ca_approved numeric, col_at_ca_protocol character varying, col_au_ca_note character varying, col_aw_ckk_ceiling numeric, col_ax_ckk_exp_q1 numeric, col_ay_ckk_exp_q2 numeric, col_az_ckk_exp_q3 numeric, col_ba_ckk_exp_q4 numeric, col_bb_ckk_rf_sched character varying, col_bc_ckk_deadline character varying, col_bd_ckk_proc_plan character varying, col_be_ckk_proc_meth character varying, col_bf_ckk_comment character varying, col_bh_cd_counterparty character varying, col_bi_cd_reference character varying, col_bj_cd_addenda character varying, col_bk_cd_subject character varying, col_bl_cd_currency character varying, col_bm_cd_ceiling numeric, col_bn_cd_exp_q1 numeric, col_bo_cd_exp_q2 numeric, col_bp_cd_exp_q3 numeric, col_bq_cd_exp_q4 numeric, col_br_cd_rf_sched character varying, col_bs_cd_vat character varying, col_bt_cd_rate numeric, col_bu_cd_amount_fx numeric, col_bv_cd_deadline character varying, col_bw_cd_scheme character varying, col_bx_cd_act character varying, col_by_cd_comment character varying, col_ca_q1_adj_current numeric, col_cb_q1_adj_ssp numeric, col_cc_q1_adj_rf numeric, col_cd_q1_adj_reserve numeric, col_ce_q1_adj_comment character varying, col_cf_q1_corrected_plan numeric, col_ch_q1_pay_date date, col_ci_q1_pay_amount numeric, col_cj_q1_pay_ho numeric, col_ck_q1_pay_rf numeric, col_cl_q1_pay_comment character varying, col_cm_q1_pay_act character varying, col_co_q1_booking numeric, col_cp_q1_actual_m1 numeric, col_cq_q1_actual_m2 numeric, col_cr_q1_actual_m3 numeric, col_cs_q1_actual_quarter numeric, col_ct_q1_rem_booking numeric, col_cu_q1_rem_actual numeric, col_cw_q1_trans_q2 numeric, col_cx_q1_trans_q3 numeric, col_cy_q1_trans_q4 numeric, col_cz_q1_economy numeric, col_da_q1_total numeric, col_dc_q2_rev_eco numeric, col_dd_q2_rev_item numeric, col_de_q2_rev_inc numeric, col_df_q2_rev_seq numeric, col_dg_q2_rev_comment character varying, col_dh_q2_new_plan numeric, col_dj_q2_adj_current numeric, col_dk_q2_adj_ssp numeric, col_dl_q2_adj_rf numeric, col_dm_q2_adj_reserve numeric, col_dn_q2_adj_comment character varying, col_do_q2_corrected_plan numeric, col_dq_q2_pay_date date, col_dr_q2_pay_amount numeric, col_ds_q2_pay_ho numeric, col_dt_q2_pay_rf numeric, col_du_q2_pay_comment character varying, col_dv_q2_pay_act character varying, col_dx_q2_booking numeric, col_dy_q2_actual_m1 numeric, col_dz_q2_actual_m2 numeric, col_ea_q2_actual_m3 numeric, col_eb_q2_actual_quarter numeric, col_ec_q2_rem_booking numeric, col_ed_q2_rem_actual numeric, col_ef_q2_trans_q3 numeric, col_eg_q2_trans_q4 numeric, col_eh_q2_economy numeric, col_ei_q2_total numeric, col_ek_q3_rev_eco numeric, col_el_q3_rev_item numeric, col_em_q3_rev_inc numeric, col_en_q3_rev_seq numeric, col_eo_q3_rev_comment character varying, col_ep_q3_new_plan numeric, col_er_q3_adj_current numeric, col_es_q3_adj_ssp numeric, col_et_q3_adj_rf numeric, col_eu_q3_adj_reserve numeric, col_ev_q3_adj_comment character varying, col_ew_q3_corrected_plan numeric, col_ey_q3_pay_date date, col_ez_q3_pay_amount numeric, col_fa_q3_pay_ho numeric, col_fb_q3_pay_rf numeric, col_fc_q3_pay_comment character varying, col_fd_q3_pay_act character varying, col_ff_q3_booking numeric, col_fg_q3_actual_m1 numeric, col_fh_q3_actual_m2 numeric, col_fi_q3_actual_m3 numeric, col_fj_q3_actual_quarter numeric, col_fk_q3_rem_booking numeric, col_fl_q3_rem_actual numeric, col_fn_q3_trans_q4 numeric, col_fo_q3_economy numeric, col_fp_q3_total numeric, col_fr_q4_rev_eco numeric, col_fs_q4_rev_item numeric, col_ft_q4_rev_inc numeric, col_fu_q4_rev_seq numeric, col_fv_q4_rev_comment character varying, col_fw_q4_new_plan numeric, col_fy_q4_adj_current numeric, col_fz_q4_adj_ssp numeric, col_ga_q4_adj_rf numeric, col_gb_q4_adj_reserve numeric, col_gc_q4_adj_comment character varying, col_gd_q4_corrected_plan numeric, col_gf_q4_pay_date date, col_gg_q4_pay_amount numeric, col_gh_q4_pay_ho numeric, col_gi_q4_pay_rf numeric, col_gj_q4_pay_comment character varying, col_gk_q4_pay_act character varying, col_gm_q4_booking numeric, col_gn_q4_actual_m1 numeric, col_go_q4_actual_m2 numeric, col_gp_q4_actual_m3 numeric, col_gq_q4_actual_quarter numeric, col_gr_q4_actual_spod numeric, col_gs_q4_rem_booking numeric, col_gt_q4_rem_actual numeric, col_gv_q4_economy numeric, col_gw_q4_total numeric, col_gz_fact_year numeric, col_hb_pay_year numeric, _sort_path integer[]) + LANGUAGE plpgsql + STABLE + SET search_path TO 'v3', 'pg_catalog' +AS $function$ +#variable_conflict use_column +DECLARE + v_year INT; + s_plan BOOL; s_cs BOOL; s_al BOOL; s_seq BOOL; s_res BOOL; + s_appr BOOL; s_ca BOOL; s_ckk BOOL; s_cd BOOL; + s_q1 BOOL; s_q2 BOOL; s_q3 BOOL; s_q4 BOOL; s_tot BOOL; + s_any_q BOOL; + s_need_ap BOOL; + s_need_bk BOOL; +BEGIN + SELECT bf.year INTO v_year FROM budget_form bf WHERE bf.id = p_form_id; + + s_plan := p_sections IS NULL OR 'plan' = ANY(p_sections); + s_cs := p_sections IS NULL OR 'contract_summary' = ANY(p_sections); + s_al := p_sections IS NULL OR 'allocation' = ANY(p_sections); + s_seq := p_sections IS NULL OR 'sequestration' = ANY(p_sections); + s_res := p_sections IS NULL OR 'reserve' = ANY(p_sections); + s_appr := p_sections IS NULL OR 'approved' = ANY(p_sections); + s_ca := p_sections IS NULL OR 'collegial' = ANY(p_sections); + s_ckk := p_sections IS NULL OR 'ckk' = ANY(p_sections); + s_cd := p_sections IS NULL OR 'contract_detail' = ANY(p_sections); + s_q1 := p_sections IS NULL OR 'q1' = ANY(p_sections); + s_q2 := p_sections IS NULL OR 'q2' = ANY(p_sections); + s_q3 := p_sections IS NULL OR 'q3' = ANY(p_sections); + s_q4 := p_sections IS NULL OR 'q4' = ANY(p_sections); + s_tot := p_sections IS NULL OR 'totals' = ANY(p_sections); + s_any_q := s_q1 OR s_q2 OR s_q3 OR s_q4 OR s_tot; + s_need_ap := s_plan OR s_appr OR s_q1 OR s_q2 OR s_q3 OR s_q4; + s_need_bk := s_q1 OR s_q2 OR s_q3 OR s_q4; + + RETURN QUERY + WITH + tw AS ( + SELECT t.id, t.section_code, t.item_id, t.num_group_id, t.name, + t.depth, t.path, t.parent_id, t.parent_item_id, t.desc_ids, + p.parent_item_id AS grandparent_item_id + FROM mv_expense_item_tree t + LEFT JOIN mv_expense_item_tree p ON p.id = t.parent_id + JOIN expense_item_form_type eft ON eft.expense_item_id = t.id + JOIN budget_form bf ON bf.id = p_form_id AND bf.form_type_code = eft.form_type_code + WHERE t.sheet = p_sheet + ), + + page AS ( + SELECT bl.id AS lid, bl.expense_item_id AS eid, bl.internal_order AS io, + bl.name AS bname + FROM budget_line bl + JOIN expense_item ei ON ei.id = bl.expense_item_id + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet + AND (p_direction IS NULL OR bl.direction IS NULL OR bl.direction = p_direction) + ), + + jp AS (SELECT p.* FROM plan p JOIN page pg ON p.line_id = pg.lid WHERE s_plan), + jcs AS (SELECT cs.* FROM contract_summary cs JOIN page pg ON cs.line_id = pg.lid WHERE s_cs), + jal AS (SELECT al.* FROM allocation al JOIN page pg ON al.line_id = pg.lid WHERE s_al), + jsd AS (SELECT sd.* FROM sequestration sd JOIN page pg ON sd.line_id = pg.lid WHERE s_seq AND sd.actor='DFIP'), + jr AS (SELECT r.* FROM reserve r JOIN page pg ON r.line_id = pg.lid WHERE s_res), + jca AS (SELECT ca.* FROM collegial_approval ca JOIN page pg ON ca.line_id = pg.lid WHERE s_ca), + jck AS (SELECT ck.* FROM ckk ck JOIN page pg ON ck.line_id = pg.lid WHERE s_ckk), + jcd AS (SELECT cd.* FROM contract_detail cd JOIN page pg ON cd.line_id = pg.lid WHERE s_cd), + jq1 AS (SELECT blq.* FROM budget_line_quarter blq JOIN page pg ON blq.line_id = pg.lid WHERE s_q1 AND blq.quarter = 1), + jq2 AS (SELECT blq.* FROM budget_line_quarter blq JOIN page pg ON blq.line_id = pg.lid WHERE s_q2 AND blq.quarter = 2), + jq3 AS (SELECT blq.* FROM budget_line_quarter blq JOIN page pg ON blq.line_id = pg.lid WHERE s_q3 AND blq.quarter = 3), + jq4 AS (SELECT blq.* FROM budget_line_quarter blq JOIN page pg ON blq.line_id = pg.lid WHERE (s_q4 OR s_tot) AND blq.quarter = 4), + jtot AS ( + SELECT blq.line_id AS tlid, + SUM(CASE WHEN blq.quarter=1 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS tot_aq1, + SUM(CASE WHEN blq.quarter=2 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS tot_aq2, + SUM(CASE WHEN blq.quarter=3 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS tot_aq3, + SUM(CASE WHEN blq.quarter=4 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0)+COALESCE(blq.actual_spod,0) END) AS tot_aq4, + SUM(CASE WHEN blq.quarter=1 THEN blq.payment_amount END) AS tot_pa1, + SUM(CASE WHEN blq.quarter=2 THEN blq.payment_amount END) AS tot_pa2, + SUM(CASE WHEN blq.quarter=3 THEN blq.payment_amount END) AS tot_pa3, + SUM(CASE WHEN blq.quarter=4 THEN blq.payment_amount END) AS tot_pa4 + FROM budget_line_quarter blq JOIN page pg ON blq.line_id = pg.lid + WHERE s_tot + GROUP BY blq.line_id + ), + + input_base AS ( + SELECT + pg.lid, pg.eid, pg.io, pg.bname, + CASE + WHEN t.depth < 3 THEN t.item_id + WHEN t.depth = 3 THEN t.parent_item_id + WHEN t.depth = 4 THEN t.grandparent_item_id + END AS sc, + t.item_id AS ic, + t.num_group_id AS ng, t.name AS ename, + COALESCE(p.plan_q1,0) AS pq1, COALESCE(p.plan_q2,0) AS pq2, + COALESCE(p.plan_q3,0) AS pq3, COALESCE(p.plan_q4,0) AS pq4, + p.comment AS pcmt, + COALESCE(sd.adj_q1,0) AS sq1, COALESCE(sd.adj_q2,0) AS sq2, + COALESCE(sd.adj_q3,0) AS sq3, COALESCE(sd.adj_q4,0) AS sq4, + sd.justification AS sjust, + COALESCE(r.amount_q1,0) AS rq1, COALESCE(r.amount_q2,0) AS rq2, + COALESCE(r.amount_q3,0) AS rq3, COALESCE(r.amount_q4,0) AS rq4, + r.justification AS rjust, + cs.*, al.internal_order AS al_io, al.property_object AS al_po, + ca.approved_amount AS ca_amt, ca.protocol_reference AS ca_prot, ca.note AS ca_note, + ck.ceiling_amount AS ckk_ceil, + COALESCE(ck.expenses_q1,0) AS ckk1, COALESCE(ck.expenses_q2,0) AS ckk2, + COALESCE(ck.expenses_q3,0) AS ckk3, COALESCE(ck.expenses_q4,0) AS ckk4, + ck.rf_schedule AS ckk_rf, ck.delivery_deadline AS ckk_dl, + ck.procurement_plan AS ckk_pp, ck.procurement_method AS ckk_pm, ck.comment AS ckk_cmt, + cd.counterparty AS cd_cp, cd.reference AS cd_ref, cd.addenda AS cd_add, + cd.subject AS cd_subj, cd.currency AS cd_cur, cd.ceiling_amount AS cd_ceil, + COALESCE(cd.expenses_q1,0) AS cdq1, COALESCE(cd.expenses_q2,0) AS cdq2, + COALESCE(cd.expenses_q3,0) AS cdq3, COALESCE(cd.expenses_q4,0) AS cdq4, + cd.rf_schedule AS cd_rf, cd.vat_rate AS cd_vat, cd.exchange_rate AS cd_rate, + cd.amount_foreign AS cd_fx, cd.deadline AS cd_dl, cd.payment_scheme AS cd_sch, + cd.act AS cd_act, cd.comment AS cd_cmt2, + q1.adj_current AS q1_ac, q1.adj_ssp AS q1_as, q1.adj_rf AS q1_ar, q1.adj_reserve AS q1_arv, + q1.adj_comment AS q1_acmt, q1.payment_date AS q1_pd, q1.payment_amount AS q1_pa, + q1.payment_amount_ho AS q1_pho, q1.payment_amount_rf AS q1_prf, + q1.payment_comment AS q1_pcmt, q1.payment_act AS q1_pact, + q1.actual_m1 AS q1_m1, q1.actual_m2 AS q1_m2, q1.actual_m3 AS q1_m3, + q1.transfer_to_q2 AS q1_tq2, q1.transfer_to_q3 AS q1_tq3, q1.transfer_to_q4 AS q1_tq4, + q2.plan_revision_eco_change AS q2_reco, q2.plan_revision_item_adj AS q2_ritm, + q2.plan_revision_increase AS q2_rinc, q2.plan_revision_sequester AS q2_rseq, + q2.plan_revision_comment AS q2_rcmt, + q2.adj_current AS q2_ac, q2.adj_ssp AS q2_as, q2.adj_rf AS q2_ar, q2.adj_reserve AS q2_arv, + q2.adj_comment AS q2_acmt, q2.payment_date AS q2_pd, q2.payment_amount AS q2_pa, + q2.payment_amount_ho AS q2_pho, q2.payment_amount_rf AS q2_prf, + q2.payment_comment AS q2_pcmt, q2.payment_act AS q2_pact, + q2.actual_m1 AS q2_m1, q2.actual_m2 AS q2_m2, q2.actual_m3 AS q2_m3, + q2.transfer_to_q3 AS q2_tq3, q2.transfer_to_q4 AS q2_tq4, + q3.plan_revision_eco_change AS q3_reco, q3.plan_revision_item_adj AS q3_ritm, + q3.plan_revision_increase AS q3_rinc, q3.plan_revision_sequester AS q3_rseq, + q3.plan_revision_comment AS q3_rcmt, + q3.adj_current AS q3_ac, q3.adj_ssp AS q3_as, q3.adj_rf AS q3_ar, q3.adj_reserve AS q3_arv, + q3.adj_comment AS q3_acmt, q3.payment_date AS q3_pd, q3.payment_amount AS q3_pa, + q3.payment_amount_ho AS q3_pho, q3.payment_amount_rf AS q3_prf, + q3.payment_comment AS q3_pcmt, q3.payment_act AS q3_pact, + q3.actual_m1 AS q3_m1, q3.actual_m2 AS q3_m2, q3.actual_m3 AS q3_m3, + q3.transfer_to_q4 AS q3_tq4, + q4.plan_revision_eco_change AS q4_reco, q4.plan_revision_item_adj AS q4_ritm, + q4.plan_revision_increase AS q4_rinc, q4.plan_revision_sequester AS q4_rseq, + q4.plan_revision_comment AS q4_rcmt, + q4.adj_current AS q4_ac, q4.adj_ssp AS q4_as, q4.adj_rf AS q4_ar, q4.adj_reserve AS q4_arv, + q4.adj_comment AS q4_acmt, q4.payment_date AS q4_pd, q4.payment_amount AS q4_pa, + q4.payment_amount_ho AS q4_pho, q4.payment_amount_rf AS q4_prf, + q4.payment_comment AS q4_pcmt, q4.payment_act AS q4_pact, + q4.actual_m1 AS q4_m1, q4.actual_m2 AS q4_m2, q4.actual_m3 AS q4_m3, + q4.actual_spod AS q4_spod, + tot.tot_aq1, tot.tot_aq2, tot.tot_aq3, tot.tot_aq4, + tot.tot_pa1, tot.tot_pa2, tot.tot_pa3, tot.tot_pa4, + CASE WHEN COALESCE(cd.expenses_q1,0)>0 THEN cd.expenses_q1 ELSE ck.expenses_q1 END AS bk1, + CASE WHEN COALESCE(cd.expenses_q2,0)>0 THEN cd.expenses_q2 ELSE ck.expenses_q2 END AS bk2, + CASE WHEN COALESCE(cd.expenses_q3,0)>0 THEN cd.expenses_q3 ELSE ck.expenses_q3 END AS bk3, + CASE WHEN COALESCE(cd.expenses_q4,0)>0 THEN cd.expenses_q4 ELSE ck.expenses_q4 END AS bk4, + COALESCE(p.plan_q1,0)+COALESCE(sd.adj_q1,0)+COALESCE(r.amount_q1,0) AS ap1, + COALESCE(p.plan_q2,0)+COALESCE(sd.adj_q2,0)+COALESCE(r.amount_q2,0) AS ap2, + COALESCE(p.plan_q3,0)+COALESCE(sd.adj_q3,0)+COALESCE(r.amount_q3,0) AS ap3, + COALESCE(p.plan_q4,0)+COALESCE(sd.adj_q4,0)+COALESCE(r.amount_q4,0) AS ap4, + t.path AS tree_path + FROM page pg + JOIN tw t ON t.id = pg.eid + LEFT JOIN jp p ON p.line_id = pg.lid + LEFT JOIN jcs cs ON cs.line_id = pg.lid + LEFT JOIN jal al ON al.line_id = pg.lid + LEFT JOIN jsd sd ON sd.line_id = pg.lid + LEFT JOIN jr r ON r.line_id = pg.lid + LEFT JOIN jca ca ON ca.line_id = pg.lid + LEFT JOIN jck ck ON ck.line_id = pg.lid + LEFT JOIN jcd cd ON cd.line_id = pg.lid + LEFT JOIN jq1 q1 ON q1.line_id = pg.lid + LEFT JOIN jq2 q2 ON q2.line_id = pg.lid + LEFT JOIN jq3 q3 ON q3.line_id = pg.lid + LEFT JOIN jq4 q4 ON q4.line_id = pg.lid + LEFT JOIN jtot tot ON tot.tlid = pg.lid + ), + enriched AS ( + SELECT b.*, + b.ap1 + COALESCE(b.q1_ac,0)+COALESCE(b.q1_as,0)+COALESCE(b.q1_ar,0)+COALESCE(b.q1_arv,0) AS cp1, + COALESCE(b.q1_m1,0)+COALESCE(b.q1_m2,0)+COALESCE(b.q1_m3,0) AS aq1, + b.ap2 + COALESCE(b.q1_tq2,0) + COALESCE(b.q2_reco,0)+COALESCE(b.q2_ritm,0)+COALESCE(b.q2_rinc,0)+COALESCE(b.q2_rseq,0) AS np2, + COALESCE(b.q2_m1,0)+COALESCE(b.q2_m2,0)+COALESCE(b.q2_m3,0) AS aq2, + b.ap3 + COALESCE(b.q1_tq3,0)+COALESCE(b.q2_tq3,0) + COALESCE(b.q3_reco,0)+COALESCE(b.q3_ritm,0)+COALESCE(b.q3_rinc,0)+COALESCE(b.q3_rseq,0) AS np3, + COALESCE(b.q3_m1,0)+COALESCE(b.q3_m2,0)+COALESCE(b.q3_m3,0) AS aq3, + b.ap4 + COALESCE(b.q1_tq4,0)+COALESCE(b.q2_tq4,0)+COALESCE(b.q3_tq4,0) + COALESCE(b.q4_reco,0)+COALESCE(b.q4_ritm,0)+COALESCE(b.q4_rinc,0)+COALESCE(b.q4_rseq,0) AS np4, + COALESCE(b.q4_m1,0)+COALESCE(b.q4_m2,0)+COALESCE(b.q4_m3,0) AS aq4_no_spod, + COALESCE(b.q4_m1,0)+COALESCE(b.q4_m2,0)+COALESCE(b.q4_m3,0)+COALESCE(b.q4_spod,0) AS aq4 + FROM input_base b + ), + final_input AS ( + SELECT e.*, + e.np2 + COALESCE(e.q2_ac,0)+COALESCE(e.q2_as,0)+COALESCE(e.q2_ar,0)+COALESCE(e.q2_arv,0) AS cp2, + e.np3 + COALESCE(e.q3_ac,0)+COALESCE(e.q3_as,0)+COALESCE(e.q3_ar,0)+COALESCE(e.q3_arv,0) AS cp3, + e.np4 + COALESCE(e.q4_ac,0)+COALESCE(e.q4_as,0)+COALESCE(e.q4_ar,0)+COALESCE(e.q4_arv,0) AS cp4 + FROM enriched e + ), + + agg AS ( + SELECT bl.expense_item_id AS eid, + SUM(p.plan_q1) AS sp1, SUM(p.plan_q2) AS sp2, SUM(p.plan_q3) AS sp3, SUM(p.plan_q4) AS sp4, + SUM(sd.adj_q1) AS ss1, SUM(sd.adj_q2) AS ss2, SUM(sd.adj_q3) AS ss3, SUM(sd.adj_q4) AS ss4, + SUM(r.amount_q1) AS sr1, SUM(r.amount_q2) AS sr2, SUM(r.amount_q3) AS sr3, SUM(r.amount_q4) AS sr4, + SUM(ca.approved_amount) AS sca, + SUM(ck.ceiling_amount) AS sckc, + SUM(ck.expenses_q1) AS sck1, SUM(ck.expenses_q2) AS sck2, SUM(ck.expenses_q3) AS sck3, SUM(ck.expenses_q4) AS sck4, + SUM(cd.ceiling_amount) AS scdc, + SUM(cd.expenses_q1) AS scd1, SUM(cd.expenses_q2) AS scd2, SUM(cd.expenses_q3) AS scd3, SUM(cd.expenses_q4) AS scd4, + SUM(cs.total_amount) AS scst, SUM(cs.future_payments_y1) AS sfy1, SUM(cs.future_payments_y2) AS sfy2, + SUM(cs.other_ssp_amount) AS sossp + FROM budget_line bl + JOIN expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN plan p ON p.line_id = bl.id AND s_need_ap + LEFT JOIN contract_summary cs ON cs.line_id = bl.id AND s_cs + LEFT JOIN sequestration sd ON sd.line_id = bl.id AND sd.actor = 'DFIP' AND s_need_ap + LEFT JOIN reserve r ON r.line_id = bl.id AND s_need_ap + LEFT JOIN collegial_approval ca ON ca.line_id = bl.id AND s_ca + LEFT JOIN ckk ck ON ck.line_id = bl.id AND (s_ckk OR s_need_bk) + LEFT JOIN contract_detail cd ON cd.line_id = bl.id AND (s_cd OR s_need_bk) + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND (p_direction is null OR bl.direction is null or bl.direction = p_direction) + AND (s_need_ap OR s_cs OR s_ca OR s_ckk OR s_cd OR s_need_bk) + GROUP BY bl.expense_item_id + ), + aq1 AS ( + SELECT bl.expense_item_id AS eid, + SUM(blq.adj_current) AS ac, SUM(blq.adj_ssp) AS as2, SUM(blq.adj_rf) AS ar, SUM(blq.adj_reserve) AS arv, + SUM(blq.payment_amount) AS pa, SUM(blq.payment_amount_ho) AS pho, SUM(blq.payment_amount_rf) AS prf, + SUM(blq.actual_m1) AS m1, SUM(blq.actual_m2) AS m2, SUM(blq.actual_m3) AS m3, + SUM(blq.transfer_to_q2) AS tq2, SUM(blq.transfer_to_q3) AS tq3, SUM(blq.transfer_to_q4) AS tq4 + FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 1 + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_q1 AND (p_direction is null OR bl.direction is null or bl.direction = p_direction) + GROUP BY bl.expense_item_id + ), + aq2 AS ( + SELECT bl.expense_item_id AS eid, + SUM(blq.plan_revision_eco_change) AS reco, SUM(blq.plan_revision_item_adj) AS ritm, + SUM(blq.plan_revision_increase) AS rinc, SUM(blq.plan_revision_sequester) AS rseq, + SUM(blq.adj_current) AS ac, SUM(blq.adj_ssp) AS as2, SUM(blq.adj_rf) AS ar, SUM(blq.adj_reserve) AS arv, + SUM(blq.payment_amount) AS pa, SUM(blq.payment_amount_ho) AS pho, SUM(blq.payment_amount_rf) AS prf, + SUM(blq.actual_m1) AS m1, SUM(blq.actual_m2) AS m2, SUM(blq.actual_m3) AS m3, + SUM(blq.transfer_to_q3) AS tq3, SUM(blq.transfer_to_q4) AS tq4 + FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 2 + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_q2 AND (p_direction is null OR bl.direction is null or bl.direction = p_direction) + GROUP BY bl.expense_item_id + ), + aq3 AS ( + SELECT bl.expense_item_id AS eid, + SUM(blq.plan_revision_eco_change) AS reco, SUM(blq.plan_revision_item_adj) AS ritm, + SUM(blq.plan_revision_increase) AS rinc, SUM(blq.plan_revision_sequester) AS rseq, + SUM(blq.adj_current) AS ac, SUM(blq.adj_ssp) AS as2, SUM(blq.adj_rf) AS ar, SUM(blq.adj_reserve) AS arv, + SUM(blq.payment_amount) AS pa, SUM(blq.payment_amount_ho) AS pho, SUM(blq.payment_amount_rf) AS prf, + SUM(blq.actual_m1) AS m1, SUM(blq.actual_m2) AS m2, SUM(blq.actual_m3) AS m3, + SUM(blq.transfer_to_q4) AS tq4 + FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 3 + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_q3 AND (p_direction is null OR bl.direction is null or bl.direction = p_direction) + GROUP BY bl.expense_item_id + ), + aq4 AS ( + SELECT bl.expense_item_id AS eid, + SUM(blq.plan_revision_eco_change) AS reco, SUM(blq.plan_revision_item_adj) AS ritm, + SUM(blq.plan_revision_increase) AS rinc, SUM(blq.plan_revision_sequester) AS rseq, + SUM(blq.adj_current) AS ac, SUM(blq.adj_ssp) AS as2, SUM(blq.adj_rf) AS ar, SUM(blq.adj_reserve) AS arv, + SUM(blq.payment_amount) AS pa, SUM(blq.payment_amount_ho) AS pho, SUM(blq.payment_amount_rf) AS prf, + SUM(blq.actual_m1) AS m1, SUM(blq.actual_m2) AS m2, SUM(blq.actual_m3) AS m3, + SUM(blq.actual_spod) AS spod + FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 4 + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND (s_q4 OR s_tot) AND (p_direction is null OR bl.direction is null or bl.direction = p_direction) + GROUP BY bl.expense_item_id + ), + atot AS ( + SELECT bl.expense_item_id AS eid, + SUM(CASE WHEN blq.quarter=1 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS fq1, + SUM(CASE WHEN blq.quarter=2 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS fq2, + SUM(CASE WHEN blq.quarter=3 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS fq3, + SUM(CASE WHEN blq.quarter=4 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0)+COALESCE(blq.actual_spod,0) END) AS fq4, + SUM(blq.payment_amount) AS pay + FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_tot AND (p_direction is null OR bl.direction is null or bl.direction = p_direction) + GROUP BY bl.expense_item_id + ), + tw_agg AS ( + SELECT tw.id, + SUM(a.sp1) AS sp1, SUM(a.sp2) AS sp2, SUM(a.sp3) AS sp3, SUM(a.sp4) AS sp4, + SUM(a.ss1) AS ss1, SUM(a.ss2) AS ss2, SUM(a.ss3) AS ss3, SUM(a.ss4) AS ss4, + SUM(a.sr1) AS sr1, SUM(a.sr2) AS sr2, SUM(a.sr3) AS sr3, SUM(a.sr4) AS sr4, + SUM(a.sca) AS sca, + SUM(a.sckc) AS sckc, SUM(a.sck1) AS sck1, SUM(a.sck2) AS sck2, SUM(a.sck3) AS sck3, SUM(a.sck4) AS sck4, + SUM(a.scdc) AS scdc, SUM(a.scd1) AS scd1, SUM(a.scd2) AS scd2, SUM(a.scd3) AS scd3, SUM(a.scd4) AS scd4, + SUM(a.scst) AS scst, SUM(a.sfy1) AS sfy1, SUM(a.sfy2) AS sfy2, SUM(a.sossp) AS sossp, + SUM(CASE WHEN COALESCE(a.scd1,0)>0 THEN a.scd1 ELSE a.sck1 END) AS bk1, + SUM(CASE WHEN COALESCE(a.scd2,0)>0 THEN a.scd2 ELSE a.sck2 END) AS bk2, + SUM(CASE WHEN COALESCE(a.scd3,0)>0 THEN a.scd3 ELSE a.sck3 END) AS bk3, + SUM(CASE WHEN COALESCE(a.scd4,0)>0 THEN a.scd4 ELSE a.sck4 END) AS bk4 + FROM tw LEFT JOIN agg a ON a.eid = ANY(tw.desc_ids) + GROUP BY tw.id + ), + tw_aq1 AS ( + SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as2) AS as2, SUM(b.ar) AS ar, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.pho) AS pho, SUM(b.prf) AS prf, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, + SUM(b.tq2) AS tq2, SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4 + FROM tw LEFT JOIN aq1 b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id + ), + tw_aq2 AS ( + SELECT tw.id, SUM(b.reco) AS reco, SUM(b.ritm) AS ritm, SUM(b.rinc) AS rinc, SUM(b.rseq) AS rseq, + SUM(b.ac) AS ac, SUM(b.as2) AS as2, SUM(b.ar) AS ar, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.pho) AS pho, SUM(b.prf) AS prf, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, + SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4 + FROM tw LEFT JOIN aq2 b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id + ), + tw_aq3 AS ( + SELECT tw.id, SUM(b.reco) AS reco, SUM(b.ritm) AS ritm, SUM(b.rinc) AS rinc, SUM(b.rseq) AS rseq, + SUM(b.ac) AS ac, SUM(b.as2) AS as2, SUM(b.ar) AS ar, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.pho) AS pho, SUM(b.prf) AS prf, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, + SUM(b.tq4) AS tq4 + FROM tw LEFT JOIN aq3 b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id + ), + tw_aq4 AS ( + SELECT tw.id, SUM(b.reco) AS reco, SUM(b.ritm) AS ritm, SUM(b.rinc) AS rinc, SUM(b.rseq) AS rseq, + SUM(b.ac) AS ac, SUM(b.as2) AS as2, SUM(b.ar) AS ar, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.pho) AS pho, SUM(b.prf) AS prf, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, + SUM(b.spod) AS spod + FROM tw LEFT JOIN aq4 b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id + ), + tw_atot AS ( + SELECT tw.id, SUM(b.fq1) AS fq1, SUM(b.fq2) AS fq2, SUM(b.fq3) AS fq3, SUM(b.fq4) AS fq4, + SUM(b.pay) AS pay + FROM tw LEFT JOIN atot b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id + ) + + SELECT * FROM ( + SELECT + 'INPUT'::VARCHAR, array_length(f.tree_path, 1)::INT, f.lid, + v_year, f.sc, f.ic, f.ng, COALESCE(f.bname, f.ename), f.io, + CASE WHEN s_plan THEN f.pq1 END, CASE WHEN s_plan THEN f.pq2 END, + CASE WHEN s_plan THEN f.pq3 END, CASE WHEN s_plan THEN f.pq4 END, + CASE WHEN s_plan THEN f.pq1+f.pq2+f.pq3+f.pq4 END, CASE WHEN s_plan THEN f.pcmt END, + CASE WHEN s_cs THEN f.total_amount END, CASE WHEN s_cs THEN f.counterparty END, + CASE WHEN s_cs THEN f.deadline END, CASE WHEN s_cs THEN f.comment END, + CASE WHEN s_cs THEN f.future_payments_y1 END, CASE WHEN s_cs THEN f.future_payments_y2 END, + CASE WHEN s_cs THEN f.other_ssp_amount END, + CASE WHEN s_al THEN f.al_io END, CASE WHEN s_al THEN f.al_po END, + CASE WHEN s_seq THEN f.sq1 END, CASE WHEN s_seq THEN f.sq2 END, + CASE WHEN s_seq THEN f.sq3 END, CASE WHEN s_seq THEN f.sq4 END, + CASE WHEN s_seq THEN f.sq1+f.sq2+f.sq3+f.sq4 END, CASE WHEN s_seq THEN f.sjust END, + CASE WHEN s_res THEN f.rq1 END, CASE WHEN s_res THEN f.rq2 END, + CASE WHEN s_res THEN f.rq3 END, CASE WHEN s_res THEN f.rq4 END, + CASE WHEN s_res THEN f.rq1+f.rq2+f.rq3+f.rq4 END, CASE WHEN s_res THEN f.rjust END, + CASE WHEN s_appr THEN f.ap1 END, CASE WHEN s_appr THEN f.ap2 END, + CASE WHEN s_appr THEN f.ap3 END, CASE WHEN s_appr THEN f.ap4 END, + CASE WHEN s_appr THEN f.ap1+f.ap2+f.ap3+f.ap4 END, + CASE WHEN s_ca THEN f.ca_amt END, CASE WHEN s_ca THEN f.ca_prot END, CASE WHEN s_ca THEN f.ca_note END, + CASE WHEN s_ckk THEN f.ckk_ceil END, CASE WHEN s_ckk THEN f.ckk1 END, CASE WHEN s_ckk THEN f.ckk2 END, + CASE WHEN s_ckk THEN f.ckk3 END, CASE WHEN s_ckk THEN f.ckk4 END, + CASE WHEN s_ckk THEN f.ckk_rf END, CASE WHEN s_ckk THEN f.ckk_dl END, + CASE WHEN s_ckk THEN f.ckk_pp END, CASE WHEN s_ckk THEN f.ckk_pm END, CASE WHEN s_ckk THEN f.ckk_cmt END, + CASE WHEN s_cd THEN f.cd_cp END, CASE WHEN s_cd THEN f.cd_ref END, CASE WHEN s_cd THEN f.cd_add END, + CASE WHEN s_cd THEN f.cd_subj END, CASE WHEN s_cd THEN f.cd_cur END, CASE WHEN s_cd THEN f.cd_ceil END, + CASE WHEN s_cd THEN f.cdq1 END, CASE WHEN s_cd THEN f.cdq2 END, + CASE WHEN s_cd THEN f.cdq3 END, CASE WHEN s_cd THEN f.cdq4 END, + CASE WHEN s_cd THEN f.cd_rf END, CASE WHEN s_cd THEN f.cd_vat END, + CASE WHEN s_cd THEN f.cd_rate END, CASE WHEN s_cd THEN f.cd_fx END, + CASE WHEN s_cd THEN f.cd_dl END, CASE WHEN s_cd THEN f.cd_sch END, + CASE WHEN s_cd THEN f.cd_act END, CASE WHEN s_cd THEN f.cd_cmt2 END, + CASE WHEN s_q1 THEN f.q1_ac END, CASE WHEN s_q1 THEN f.q1_as END, + CASE WHEN s_q1 THEN f.q1_ar END, CASE WHEN s_q1 THEN f.q1_arv END, + CASE WHEN s_q1 THEN f.q1_acmt END, CASE WHEN s_q1 THEN f.cp1 END, + CASE WHEN s_q1 THEN f.q1_pd END, CASE WHEN s_q1 THEN f.q1_pa END, + CASE WHEN s_q1 THEN f.q1_pho END, CASE WHEN s_q1 THEN f.q1_prf END, + CASE WHEN s_q1 THEN f.q1_pcmt END, CASE WHEN s_q1 THEN f.q1_pact END, + CASE WHEN s_q1 THEN COALESCE(f.bk1,0) END, + CASE WHEN s_q1 THEN f.q1_m1 END, CASE WHEN s_q1 THEN f.q1_m2 END, CASE WHEN s_q1 THEN f.q1_m3 END, + CASE WHEN s_q1 THEN f.aq1 END, + CASE WHEN s_q1 THEN f.cp1 - COALESCE(f.bk1,0) END, + CASE WHEN s_q1 THEN f.cp1 - f.aq1 END, + CASE WHEN s_q1 THEN f.q1_tq2 END, CASE WHEN s_q1 THEN f.q1_tq3 END, CASE WHEN s_q1 THEN f.q1_tq4 END, + CASE WHEN s_q1 THEN f.cp1 - f.aq1 - COALESCE(f.q1_tq2,0) - COALESCE(f.q1_tq3,0) - COALESCE(f.q1_tq4,0) END, + CASE WHEN s_q1 THEN f.cp1 - f.aq1 END, + CASE WHEN s_q2 THEN f.q2_reco END, CASE WHEN s_q2 THEN f.q2_ritm END, + CASE WHEN s_q2 THEN f.q2_rinc END, CASE WHEN s_q2 THEN f.q2_rseq END, + CASE WHEN s_q2 THEN f.q2_rcmt END, CASE WHEN s_q2 THEN f.np2 END, + CASE WHEN s_q2 THEN f.q2_ac END, CASE WHEN s_q2 THEN f.q2_as END, + CASE WHEN s_q2 THEN f.q2_ar END, CASE WHEN s_q2 THEN f.q2_arv END, + CASE WHEN s_q2 THEN f.q2_acmt END, CASE WHEN s_q2 THEN f.cp2 END, + CASE WHEN s_q2 THEN f.q2_pd END, CASE WHEN s_q2 THEN f.q2_pa END, + CASE WHEN s_q2 THEN f.q2_pho END, CASE WHEN s_q2 THEN f.q2_prf END, + CASE WHEN s_q2 THEN f.q2_pcmt END, CASE WHEN s_q2 THEN f.q2_pact END, + CASE WHEN s_q2 THEN COALESCE(f.bk2,0) END, + CASE WHEN s_q2 THEN f.q2_m1 END, CASE WHEN s_q2 THEN f.q2_m2 END, CASE WHEN s_q2 THEN f.q2_m3 END, + CASE WHEN s_q2 THEN f.aq2 END, + CASE WHEN s_q2 THEN f.cp2 - COALESCE(f.bk2,0) END, + CASE WHEN s_q2 THEN f.cp2 - f.aq2 END, + CASE WHEN s_q2 THEN f.q2_tq3 END, CASE WHEN s_q2 THEN f.q2_tq4 END, + CASE WHEN s_q2 THEN f.cp2 - f.aq2 - COALESCE(f.q2_tq3,0) - COALESCE(f.q2_tq4,0) END, + CASE WHEN s_q2 THEN f.cp2 - f.aq2 END, + CASE WHEN s_q3 THEN f.q3_reco END, CASE WHEN s_q3 THEN f.q3_ritm END, + CASE WHEN s_q3 THEN f.q3_rinc END, CASE WHEN s_q3 THEN f.q3_rseq END, + CASE WHEN s_q3 THEN f.q3_rcmt END, CASE WHEN s_q3 THEN f.np3 END, + CASE WHEN s_q3 THEN f.q3_ac END, CASE WHEN s_q3 THEN f.q3_as END, + CASE WHEN s_q3 THEN f.q3_ar END, CASE WHEN s_q3 THEN f.q3_arv END, + CASE WHEN s_q3 THEN f.q3_acmt END, CASE WHEN s_q3 THEN f.cp3 END, + CASE WHEN s_q3 THEN f.q3_pd END, CASE WHEN s_q3 THEN f.q3_pa END, + CASE WHEN s_q3 THEN f.q3_pho END, CASE WHEN s_q3 THEN f.q3_prf END, + CASE WHEN s_q3 THEN f.q3_pcmt END, CASE WHEN s_q3 THEN f.q3_pact END, + CASE WHEN s_q3 THEN COALESCE(f.bk3,0) END, + CASE WHEN s_q3 THEN f.q3_m1 END, CASE WHEN s_q3 THEN f.q3_m2 END, CASE WHEN s_q3 THEN f.q3_m3 END, + CASE WHEN s_q3 THEN f.aq3 END, + CASE WHEN s_q3 THEN f.cp3 - COALESCE(f.bk3,0) END, + CASE WHEN s_q3 THEN f.cp3 - f.aq3 END, + CASE WHEN s_q3 THEN f.q3_tq4 END, + CASE WHEN s_q3 THEN f.cp3 - f.aq3 - COALESCE(f.q3_tq4,0) END, + CASE WHEN s_q3 THEN f.cp3 - f.aq3 END, + CASE WHEN s_q4 THEN f.q4_reco END, CASE WHEN s_q4 THEN f.q4_ritm END, + CASE WHEN s_q4 THEN f.q4_rinc END, CASE WHEN s_q4 THEN f.q4_rseq END, + CASE WHEN s_q4 THEN f.q4_rcmt END, CASE WHEN s_q4 THEN f.np4 END, + CASE WHEN s_q4 THEN f.q4_ac END, CASE WHEN s_q4 THEN f.q4_as END, + CASE WHEN s_q4 THEN f.q4_ar END, CASE WHEN s_q4 THEN f.q4_arv END, + CASE WHEN s_q4 THEN f.q4_acmt END, CASE WHEN s_q4 THEN f.cp4 END, + CASE WHEN s_q4 THEN f.q4_pd END, CASE WHEN s_q4 THEN f.q4_pa END, + CASE WHEN s_q4 THEN f.q4_pho END, CASE WHEN s_q4 THEN f.q4_prf END, + CASE WHEN s_q4 THEN f.q4_pcmt END, CASE WHEN s_q4 THEN f.q4_pact END, + CASE WHEN s_q4 THEN COALESCE(f.bk4,0) END, + CASE WHEN s_q4 THEN f.q4_m1 END, CASE WHEN s_q4 THEN f.q4_m2 END, CASE WHEN s_q4 THEN f.q4_m3 END, + CASE WHEN s_q4 THEN f.aq4_no_spod END, + CASE WHEN s_q4 THEN f.q4_spod END, + CASE WHEN s_q4 THEN f.cp4 - COALESCE(f.bk4,0) END, + CASE WHEN s_q4 THEN f.cp4 - f.aq4 END, + CASE WHEN s_q4 THEN f.cp4 - f.aq4 END, + CASE WHEN s_q4 THEN f.cp4 - f.aq4 END, + CASE WHEN s_tot THEN COALESCE(f.tot_aq1,0)+COALESCE(f.tot_aq2,0)+COALESCE(f.tot_aq3,0)+COALESCE(f.tot_aq4,0) END, + CASE WHEN s_tot THEN COALESCE(f.tot_pa1,0)+COALESCE(f.tot_pa2,0)+COALESCE(f.tot_pa3,0)+COALESCE(f.tot_pa4,0) END, + f.tree_path || ARRAY[f.lid] AS _sort + FROM final_input f + + UNION ALL + + SELECT + CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' WHEN 4 THEN 'SUB_SUB_ITEM' END::VARCHAR, + t.depth, NULL::INT, + v_year, + CASE + WHEN t.depth < 3 THEN t.item_id + WHEN t.depth = 3 THEN t.parent_item_id + WHEN t.depth = 4 THEN t.grandparent_item_id + END, + CASE WHEN t.depth >= 3 THEN t.item_id ELSE NULL::VARCHAR END, + t.num_group_id, t.name, NULL::VARCHAR, + CASE WHEN s_plan THEN ta.sp1 END, + CASE WHEN s_plan THEN ta.sp2 END, + CASE WHEN s_plan THEN ta.sp3 END, + CASE WHEN s_plan THEN ta.sp4 END, + CASE WHEN s_plan THEN (COALESCE(ta.sp1,0)+COALESCE(ta.sp2,0)+COALESCE(ta.sp3,0)+COALESCE(ta.sp4,0)) END, + NULL::VARCHAR, + CASE WHEN s_cs THEN ta.scst END, + NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_cs THEN ta.sfy1 END, + CASE WHEN s_cs THEN ta.sfy2 END, + CASE WHEN s_cs THEN ta.sossp END, + NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_seq THEN ta.ss1 END, + CASE WHEN s_seq THEN ta.ss2 END, + CASE WHEN s_seq THEN ta.ss3 END, + CASE WHEN s_seq THEN ta.ss4 END, + CASE WHEN s_seq THEN (COALESCE(ta.ss1,0)+COALESCE(ta.ss2,0)+COALESCE(ta.ss3,0)+COALESCE(ta.ss4,0)) END, + NULL::VARCHAR, + CASE WHEN s_res THEN ta.sr1 END, + CASE WHEN s_res THEN ta.sr2 END, + CASE WHEN s_res THEN ta.sr3 END, + CASE WHEN s_res THEN ta.sr4 END, + CASE WHEN s_res THEN (COALESCE(ta.sr1,0)+COALESCE(ta.sr2,0)+COALESCE(ta.sr3,0)+COALESCE(ta.sr4,0)) END, + NULL::VARCHAR, + CASE WHEN s_appr THEN (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) END, + CASE WHEN s_appr THEN (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) END, + CASE WHEN s_appr THEN (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) END, + CASE WHEN s_appr THEN (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) END, + CASE WHEN s_appr THEN (COALESCE(ta.sp1,0)+COALESCE(ta.sp2,0)+COALESCE(ta.sp3,0)+COALESCE(ta.sp4,0) + +COALESCE(ta.ss1,0)+COALESCE(ta.ss2,0)+COALESCE(ta.ss3,0)+COALESCE(ta.ss4,0) + +COALESCE(ta.sr1,0)+COALESCE(ta.sr2,0)+COALESCE(ta.sr3,0)+COALESCE(ta.sr4,0)) END, + CASE WHEN s_ca THEN ta.sca END, + NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_ckk THEN ta.sckc END, + CASE WHEN s_ckk THEN ta.sck1 END, + CASE WHEN s_ckk THEN ta.sck2 END, + CASE WHEN s_ckk THEN ta.sck3 END, + CASE WHEN s_ckk THEN ta.sck4 END, + NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, + NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_cd THEN ta.scdc END, + CASE WHEN s_cd THEN ta.scd1 END, + CASE WHEN s_cd THEN ta.scd2 END, + CASE WHEN s_cd THEN ta.scd3 END, + CASE WHEN s_cd THEN ta.scd4 END, + NULL::VARCHAR, NULL::VARCHAR, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_q1 THEN tb1.ac END, + CASE WHEN s_q1 THEN tb1.as2 END, + CASE WHEN s_q1 THEN tb1.ar END, + CASE WHEN s_q1 THEN tb1.arv END, + NULL::VARCHAR, + CASE WHEN s_q1 THEN + (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) + + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)), 0) + END, + NULL::DATE, + CASE WHEN s_q1 THEN tb1.pa END, + CASE WHEN s_q1 THEN tb1.pho END, + CASE WHEN s_q1 THEN tb1.prf END, + NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_q1 THEN ta.bk1 END, + CASE WHEN s_q1 THEN tb1.m1 END, + CASE WHEN s_q1 THEN tb1.m2 END, + CASE WHEN s_q1 THEN tb1.m3 END, + CASE WHEN s_q1 THEN (COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)) END, + CASE WHEN s_q1 THEN + (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) + + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)),0) + - COALESCE(ta.bk1,0) + END, + CASE WHEN s_q1 THEN + (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) + + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)),0) + - COALESCE((COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)),0) + END, + CASE WHEN s_q1 THEN tb1.tq2 END, + CASE WHEN s_q1 THEN tb1.tq3 END, + CASE WHEN s_q1 THEN tb1.tq4 END, + CASE WHEN s_q1 THEN + (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) + + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)),0) + - COALESCE((COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)),0) + - COALESCE((COALESCE(tb1.tq2,0)+COALESCE(tb1.tq3,0)+COALESCE(tb1.tq4,0)),0) + END, + CASE WHEN s_q1 THEN + (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) + + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)),0) + - COALESCE((COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)),0) + END, + CASE WHEN s_q2 THEN tb2.reco END, + CASE WHEN s_q2 THEN tb2.ritm END, + CASE WHEN s_q2 THEN tb2.rinc END, + CASE WHEN s_q2 THEN tb2.rseq END, + NULL::VARCHAR, + CASE WHEN s_q2 THEN + (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) + + COALESCE(tb1.tq2, 0) + + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)), 0) + END, + CASE WHEN s_q2 THEN tb2.ac END, + CASE WHEN s_q2 THEN tb2.as2 END, + CASE WHEN s_q2 THEN tb2.ar END, + CASE WHEN s_q2 THEN tb2.arv END, + NULL::VARCHAR, + CASE WHEN s_q2 THEN + (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) + + COALESCE(tb1.tq2, 0) + + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)), 0) + END, + NULL::DATE, + CASE WHEN s_q2 THEN tb2.pa END, + CASE WHEN s_q2 THEN tb2.pho END, + CASE WHEN s_q2 THEN tb2.prf END, + NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_q2 THEN ta.bk2 END, + CASE WHEN s_q2 THEN tb2.m1 END, + CASE WHEN s_q2 THEN tb2.m2 END, + CASE WHEN s_q2 THEN tb2.m3 END, + CASE WHEN s_q2 THEN (COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)) END, + CASE WHEN s_q2 THEN + (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) + + COALESCE(tb1.tq2,0) + + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)),0) + - COALESCE(ta.bk2,0) + END, + CASE WHEN s_q2 THEN + (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) + + COALESCE(tb1.tq2,0) + + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)),0) + - COALESCE((COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)),0) + END, + CASE WHEN s_q2 THEN tb2.tq3 END, + CASE WHEN s_q2 THEN tb2.tq4 END, + CASE WHEN s_q2 THEN + (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) + + COALESCE(tb1.tq2,0) + + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)),0) + - COALESCE((COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)),0) + - COALESCE((COALESCE(tb2.tq3,0)+COALESCE(tb2.tq4,0)),0) + END, + CASE WHEN s_q2 THEN + (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) + + COALESCE(tb1.tq2,0) + + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)),0) + - COALESCE((COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)),0) + END, + CASE WHEN s_q3 THEN tb3.reco END, + CASE WHEN s_q3 THEN tb3.ritm END, + CASE WHEN s_q3 THEN tb3.rinc END, + CASE WHEN s_q3 THEN tb3.rseq END, + NULL::VARCHAR, + CASE WHEN s_q3 THEN + (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) + + COALESCE(tb1.tq3, 0) + + COALESCE(tb2.tq3, 0) + + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)), 0) + END, + CASE WHEN s_q3 THEN tb3.ac END, + CASE WHEN s_q3 THEN tb3.as2 END, + CASE WHEN s_q3 THEN tb3.ar END, + CASE WHEN s_q3 THEN tb3.arv END, + NULL::VARCHAR, + CASE WHEN s_q3 THEN + (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) + + COALESCE(tb1.tq3, 0) + + COALESCE(tb2.tq3, 0) + + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)), 0) + END, + NULL::DATE, + CASE WHEN s_q3 THEN tb3.pa END, + CASE WHEN s_q3 THEN tb3.pho END, + CASE WHEN s_q3 THEN tb3.prf END, + NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_q3 THEN ta.bk3 END, + CASE WHEN s_q3 THEN tb3.m1 END, + CASE WHEN s_q3 THEN tb3.m2 END, + CASE WHEN s_q3 THEN tb3.m3 END, + CASE WHEN s_q3 THEN (COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)) END, + CASE WHEN s_q3 THEN + (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) + + COALESCE(tb1.tq3,0) + + COALESCE(tb2.tq3,0) + + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)),0) + - COALESCE(ta.bk3,0) + END, + CASE WHEN s_q3 THEN + (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) + + COALESCE(tb1.tq3,0) + + COALESCE(tb2.tq3,0) + + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)),0) + - COALESCE((COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)),0) + END, + CASE WHEN s_q3 THEN tb3.tq4 END, + CASE WHEN s_q3 THEN + (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) + + COALESCE(tb1.tq3,0) + + COALESCE(tb2.tq3,0) + + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)),0) + - COALESCE((COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)),0) + - COALESCE(COALESCE(tb3.tq4,0),0) + END, + CASE WHEN s_q3 THEN + (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) + + COALESCE(tb1.tq3,0) + + COALESCE(tb2.tq3,0) + + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)),0) + - COALESCE((COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)),0) + END, + CASE WHEN s_q4 THEN tb4.reco END, + CASE WHEN s_q4 THEN tb4.ritm END, + CASE WHEN s_q4 THEN tb4.rinc END, + CASE WHEN s_q4 THEN tb4.rseq END, + NULL::VARCHAR, + CASE WHEN s_q4 THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) + + COALESCE(tb1.tq4, 0) + + COALESCE(tb2.tq4, 0) + + COALESCE(tb3.tq4, 0) + + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)), 0) + END, + CASE WHEN s_q4 THEN tb4.ac END, + CASE WHEN s_q4 THEN tb4.as2 END, + CASE WHEN s_q4 THEN tb4.ar END, + CASE WHEN s_q4 THEN tb4.arv END, + NULL::VARCHAR, + CASE WHEN s_q4 THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) + + COALESCE(tb1.tq4, 0) + + COALESCE(tb2.tq4, 0) + + COALESCE(tb3.tq4, 0) + + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)), 0) + END, + NULL::DATE, + CASE WHEN s_q4 THEN tb4.pa END, + CASE WHEN s_q4 THEN tb4.pho END, + CASE WHEN s_q4 THEN tb4.prf END, + NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_q4 THEN ta.bk4 END, + CASE WHEN s_q4 THEN tb4.m1 END, + CASE WHEN s_q4 THEN tb4.m2 END, + CASE WHEN s_q4 THEN tb4.m3 END, + CASE WHEN s_q4 THEN (COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)) END, + CASE WHEN s_q4 THEN tb4.spod END, + CASE WHEN s_q4 THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) + + COALESCE(tb1.tq4,0) + + COALESCE(tb2.tq4,0) + + COALESCE(tb3.tq4,0) + + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)),0) + - COALESCE(ta.bk4,0) + END, + CASE WHEN s_q4 THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) + + COALESCE(tb1.tq4,0) + + COALESCE(tb2.tq4,0) + + COALESCE(tb3.tq4,0) + + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)),0) + - COALESCE((COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)),0) + END, + CASE WHEN s_q4 THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) + + COALESCE(tb1.tq4,0) + + COALESCE(tb2.tq4,0) + + COALESCE(tb3.tq4,0) + + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)),0) + - COALESCE((COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)),0) + END, + CASE WHEN s_q4 THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) + + COALESCE(tb1.tq4,0) + + COALESCE(tb2.tq4,0) + + COALESCE(tb3.tq4,0) + + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)),0) + - COALESCE((COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)),0) + END, + CASE WHEN s_tot THEN (COALESCE(tt.fq1,0)+COALESCE(tt.fq2,0)+COALESCE(tt.fq3,0)+COALESCE(tt.fq4,0)) END, + CASE WHEN s_tot THEN tt.pay END, + t.path AS _sort + + FROM tw t + LEFT JOIN tw_agg ta ON ta.id = t.id + LEFT JOIN tw_aq1 tb1 ON tb1.id = t.id + LEFT JOIN tw_aq2 tb2 ON tb2.id = t.id + LEFT JOIN tw_aq3 tb3 ON tb3.id = t.id + LEFT JOIN tw_aq4 tb4 ON tb4.id = t.id + LEFT JOIN tw_atot tt ON tt.id = t.id + ) sub + ORDER BY sub._sort; + +END; +$function$ +; + +CREATE OR REPLACE FUNCTION v3.v_form1_sheet_jsonb(p_form_id integer, p_sheet character varying, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[]) + RETURNS TABLE(row_type character varying, depth integer, line_id integer, header jsonb, plan_data jsonb, contract_summary jsonb, allocation_data jsonb, sequestration_data jsonb, reserve_data jsonb, approved_data jsonb, collegial_data jsonb, ckk_data jsonb, contract_detail jsonb, q1_data jsonb, q2_data jsonb, q3_data jsonb, q4_data jsonb, totals_data jsonb, _sort_path integer[]) + LANGUAGE sql + STABLE + SET search_path TO 'v3', 'pg_catalog' +AS $function$ + WITH flags AS ( + SELECT + (p_sections IS NULL OR 'plan' = ANY(p_sections)) AS s_plan, + (p_sections IS NULL OR 'contract_summary' = ANY(p_sections)) AS s_cs, + (p_sections IS NULL OR 'allocation' = ANY(p_sections)) AS s_al, + (p_sections IS NULL OR 'sequestration' = ANY(p_sections)) AS s_seq, + (p_sections IS NULL OR 'reserve' = ANY(p_sections)) AS s_res, + (p_sections IS NULL OR 'approved' = ANY(p_sections)) AS s_appr, + (p_sections IS NULL OR 'collegial' = ANY(p_sections)) AS s_ca, + (p_sections IS NULL OR 'ckk' = ANY(p_sections)) AS s_ckk, + (p_sections IS NULL OR 'contract_detail' = ANY(p_sections)) AS s_cd, + (p_sections IS NULL OR 'q1' = ANY(p_sections)) AS s_q1, + (p_sections IS NULL OR 'q2' = ANY(p_sections)) AS s_q2, + (p_sections IS NULL OR 'q3' = ANY(p_sections)) AS s_q3, + (p_sections IS NULL OR 'q4' = ANY(p_sections)) AS s_q4, + (p_sections IS NULL OR 'totals' = ANY(p_sections)) AS s_tot + ) + SELECT + s.row_type, s.depth, s.line_id, + + jsonb_build_object( + 'year', s.col_B_year, + 'section', s.col_C_section, + 'item_id', s.col_D_item_id, + 'num_group', s.col_E_num_group, + 'name', s.col_F_name, + 'internal_order', s.col_G_internal_order, + 'expense_item_id', CASE WHEN s.row_type = 'INPUT' + THEN s._sort_path[array_upper(s._sort_path,1)-1] + ELSE s._sort_path[array_upper(s._sort_path,1)] END + ) AS header, + + CASE WHEN f.s_plan THEN jsonb_build_object( + 'q1', s.col_H_plan_q1, + 'q2', s.col_I_plan_q2, + 'q3', s.col_J_plan_q3, + 'q4', s.col_K_plan_q4, + 'year', s.col_L_plan_year, + 'comment', s.col_M_plan_comment + ) END AS plan_data, + + CASE WHEN f.s_cs THEN jsonb_build_object( + 'total', s.col_N_cs_total, + 'counterparty', s.col_O_cs_counterparty, + 'deadline', s.col_P_cs_deadline, + 'comment', s.col_Q_cs_comment, + 'future_y1', s.col_R_cs_future_y1, + 'future_y2', s.col_S_cs_future_y2, + 'other_ssp', s.col_T_cs_other_ssp + ) END AS contract_summary, + + CASE WHEN f.s_al THEN jsonb_build_object( + 'order', s.col_V_al_order, + 'property', s.col_W_al_property + ) END AS allocation_data, + + CASE WHEN f.s_seq THEN jsonb_build_object( + 'q1', s.col_Y_seq_q1, + 'q2', s.col_Z_seq_q2, + 'q3', s.col_AA_seq_q3, + 'q4', s.col_AB_seq_q4, + 'year', s.col_AC_seq_year, + 'justification', s.col_AD_seq_justification + ) END AS sequestration_data, + + CASE WHEN f.s_res THEN jsonb_build_object( + 'q1', s.col_AF_res_q1, + 'q2', s.col_AG_res_q2, + 'q3', s.col_AH_res_q3, + 'q4', s.col_AI_res_q4, + 'year', s.col_AJ_res_year, + 'justification', s.col_AK_res_justification + ) END AS reserve_data, + + CASE WHEN f.s_appr THEN jsonb_build_object( + 'q1', s.col_AM_approved_q1, + 'q2', s.col_AN_approved_q2, + 'q3', s.col_AO_approved_q3, + 'q4', s.col_AP_approved_q4, + 'year', s.col_AQ_approved_year + ) END AS approved_data, + + CASE WHEN f.s_ca THEN jsonb_build_object( + 'approved', s.col_AS_ca_approved, + 'protocol', s.col_AT_ca_protocol, + 'note', s.col_AU_ca_note + ) END AS collegial_data, + + CASE WHEN f.s_ckk THEN jsonb_build_object( + 'ceiling', s.col_AW_ckk_ceiling, + 'q1', s.col_AX_ckk_exp_q1, + 'q2', s.col_AY_ckk_exp_q2, + 'q3', s.col_AZ_ckk_exp_q3, + 'q4', s.col_BA_ckk_exp_q4, + 'rf_schedule', s.col_BB_ckk_rf_sched, + 'deadline', s.col_BC_ckk_deadline, + 'proc_plan', s.col_BD_ckk_proc_plan, + 'proc_method', s.col_BE_ckk_proc_meth, + 'comment', s.col_BF_ckk_comment + ) END AS ckk_data, + + CASE WHEN f.s_cd THEN jsonb_build_object( + 'counterparty', s.col_BH_cd_counterparty, + 'reference', s.col_BI_cd_reference, + 'addenda', s.col_BJ_cd_addenda, + 'subject', s.col_BK_cd_subject, + 'currency', s.col_BL_cd_currency, + 'ceiling', s.col_BM_cd_ceiling, + 'q1', s.col_BN_cd_exp_q1, + 'q2', s.col_BO_cd_exp_q2, + 'q3', s.col_BP_cd_exp_q3, + 'q4', s.col_BQ_cd_exp_q4, + 'rf_schedule', s.col_BR_cd_rf_sched, + 'vat_rate', s.col_BS_cd_vat, + 'exchange_rate', s.col_BT_cd_rate, + 'amount_foreign', s.col_BU_cd_amount_fx, + 'deadline', s.col_BV_cd_deadline, + 'payment_scheme', s.col_BW_cd_scheme, + 'act', s.col_BX_cd_act, + 'comment', s.col_BY_cd_comment + ) END AS contract_detail, + + CASE WHEN f.s_q1 THEN jsonb_build_object( + 'adj_current', s.col_CA_q1_adj_current, + 'adj_ssp', s.col_CB_q1_adj_ssp, + 'adj_rf', s.col_CC_q1_adj_rf, + 'adj_reserve', s.col_CD_q1_adj_reserve, + 'adj_comment', s.col_CE_q1_adj_comment, + 'corrected_plan', s.col_CF_q1_corrected_plan, + 'pay_date', TO_CHAR(s.col_CH_q1_pay_date, 'DD.MM.YYYY'), + 'pay_amount', s.col_CI_q1_pay_amount, + 'pay_ho', s.col_CJ_q1_pay_ho, + 'pay_rf', s.col_CK_q1_pay_rf, + 'pay_comment', s.col_CL_q1_pay_comment, + 'pay_act', s.col_CM_q1_pay_act, + 'booking', s.col_CO_q1_booking, + 'actual_m1', s.col_CP_q1_actual_m1, + 'actual_m2', s.col_CQ_q1_actual_m2, + 'actual_m3', s.col_CR_q1_actual_m3, + 'actual_quarter', s.col_CS_q1_actual_quarter, + 'rem_booking', s.col_CT_q1_rem_booking, + 'rem_actual', s.col_CU_q1_rem_actual, + 'transfer_q2', s.col_CW_q1_trans_q2, + 'transfer_q3', s.col_CX_q1_trans_q3, + 'transfer_q4', s.col_CY_q1_trans_q4, + 'economy', s.col_CZ_q1_economy, + 'total', s.col_DA_q1_total + ) END AS q1_data, + + CASE WHEN f.s_q2 THEN jsonb_build_object( + 'rev_eco', s.col_DC_q2_rev_eco, + 'rev_item', s.col_DD_q2_rev_item, + 'rev_inc', s.col_DE_q2_rev_inc, + 'rev_seq', s.col_DF_q2_rev_seq, + 'rev_comment', s.col_DG_q2_rev_comment, + 'new_plan', s.col_DH_q2_new_plan, + 'adj_current', s.col_DJ_q2_adj_current, + 'adj_ssp', s.col_DK_q2_adj_ssp, + 'adj_rf', s.col_DL_q2_adj_rf, + 'adj_reserve', s.col_DM_q2_adj_reserve, + 'adj_comment', s.col_DN_q2_adj_comment, + 'corrected_plan', s.col_DO_q2_corrected_plan, + 'pay_date', TO_CHAR(s.col_DQ_q2_pay_date, 'DD.MM.YYYY'), + 'pay_amount', s.col_DR_q2_pay_amount, + 'pay_ho', s.col_DS_q2_pay_ho, + 'pay_rf', s.col_DT_q2_pay_rf, + 'pay_comment', s.col_DU_q2_pay_comment, + 'pay_act', s.col_DV_q2_pay_act, + 'booking', s.col_DX_q2_booking, + 'actual_m1', s.col_DY_q2_actual_m1, + 'actual_m2', s.col_DZ_q2_actual_m2, + 'actual_m3', s.col_EA_q2_actual_m3, + 'actual_quarter', s.col_EB_q2_actual_quarter, + 'rem_booking', s.col_EC_q2_rem_booking, + 'rem_actual', s.col_ED_q2_rem_actual, + 'transfer_q3', s.col_EF_q2_trans_q3, + 'transfer_q4', s.col_EG_q2_trans_q4, + 'economy', s.col_EH_q2_economy, + 'total', s.col_EI_q2_total + ) END AS q2_data, + + CASE WHEN f.s_q3 THEN jsonb_build_object( + 'rev_eco', s.col_EK_q3_rev_eco, + 'rev_item', s.col_EL_q3_rev_item, + 'rev_inc', s.col_EM_q3_rev_inc, + 'rev_seq', s.col_EN_q3_rev_seq, + 'rev_comment', s.col_EO_q3_rev_comment, + 'new_plan', s.col_EP_q3_new_plan, + 'adj_current', s.col_ER_q3_adj_current, + 'adj_ssp', s.col_ES_q3_adj_ssp, + 'adj_rf', s.col_ET_q3_adj_rf, + 'adj_reserve', s.col_EU_q3_adj_reserve, + 'adj_comment', s.col_EV_q3_adj_comment, + 'corrected_plan', s.col_EW_q3_corrected_plan, + 'pay_date', TO_CHAR(s.col_EY_q3_pay_date, 'DD.MM.YYYY'), + 'pay_amount', s.col_EZ_q3_pay_amount, + 'pay_ho', s.col_FA_q3_pay_ho, + 'pay_rf', s.col_FB_q3_pay_rf, + 'pay_comment', s.col_FC_q3_pay_comment, + 'pay_act', s.col_FD_q3_pay_act, + 'booking', s.col_FF_q3_booking, + 'actual_m1', s.col_FG_q3_actual_m1, + 'actual_m2', s.col_FH_q3_actual_m2, + 'actual_m3', s.col_FI_q3_actual_m3, + 'actual_quarter', s.col_FJ_q3_actual_quarter, + 'rem_booking', s.col_FK_q3_rem_booking, + 'rem_actual', s.col_FL_q3_rem_actual, + 'transfer_q4', s.col_FN_q3_trans_q4, + 'economy', s.col_FO_q3_economy, + 'total', s.col_FP_q3_total + ) END AS q3_data, + + CASE WHEN f.s_q4 THEN jsonb_build_object( + 'rev_eco', s.col_FR_q4_rev_eco, + 'rev_item', s.col_FS_q4_rev_item, + 'rev_inc', s.col_FT_q4_rev_inc, + 'rev_seq', s.col_FU_q4_rev_seq, + 'rev_comment', s.col_FV_q4_rev_comment, + 'new_plan', s.col_FW_q4_new_plan, + 'adj_current', s.col_FY_q4_adj_current, + 'adj_ssp', s.col_FZ_q4_adj_ssp, + 'adj_rf', s.col_GA_q4_adj_rf, + 'adj_reserve', s.col_GB_q4_adj_reserve, + 'adj_comment', s.col_GC_q4_adj_comment, + 'corrected_plan', s.col_GD_q4_corrected_plan, + 'pay_date', TO_CHAR(s.col_GF_q4_pay_date, 'DD.MM.YYYY'), + 'pay_amount', s.col_GG_q4_pay_amount, + 'pay_ho', s.col_GH_q4_pay_ho, + 'pay_rf', s.col_GI_q4_pay_rf, + 'pay_comment', s.col_GJ_q4_pay_comment, + 'pay_act', s.col_GK_q4_pay_act, + 'booking', s.col_GM_q4_booking, + 'actual_m1', s.col_GN_q4_actual_m1, + 'actual_m2', s.col_GO_q4_actual_m2, + 'actual_m3', s.col_GP_q4_actual_m3, + 'actual_quarter', s.col_GQ_q4_actual_quarter, + 'actual_spod', s.col_GR_q4_actual_spod, + 'rem_booking', s.col_GS_q4_rem_booking, + 'rem_actual', s.col_GT_q4_rem_actual, + 'economy', s.col_GV_q4_economy, + 'total', s.col_GW_q4_total + ) END AS q4_data, + + CASE WHEN f.s_tot THEN jsonb_build_object( + 'fact_year', s.col_GZ_fact_year, + 'pay_year', s.col_HB_pay_year + ) END AS totals_data, + + s._sort_path + FROM v_form1_sheet_sections(p_form_id, p_sheet, p_direction, p_sections) s + CROSS JOIN flags f + ORDER BY s._sort_path; +$function$ +; + +CREATE OR REPLACE FUNCTION v3.v_form_view(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[], p_direction character varying DEFAULT NULL::character varying, p_user_id integer DEFAULT NULL::integer) + RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) + LANGUAGE plpgsql + STABLE +AS $function$ +DECLARE + v_form_type VARCHAR; + v_year INT; + v_org_unit_id INT; + v_user_id INT; +BEGIN + SELECT form_type_code, year, org_unit_id + INTO v_form_type, v_year, v_org_unit_id + FROM v3.budget_form + WHERE id = p_form_id; + + IF NOT FOUND THEN + RAISE EXCEPTION 'budget_form id=% не существует в v3.budget_form', p_form_id; + END IF; + + v_user_id := p_user_id; + IF p_user_id IS NULL THEN + v_user_id := v3.current_user_id(); + END IF; + RETURN QUERY + SELECT + 'META'::VARCHAR, + -1, + 0::BIGINT, + jsonb_build_object( + 'user_id', v_user_id, + 'role', v3.user_role_code(v_user_id), + 'sheet', p_sheet, + 'editable', COALESCE( + (SELECT jsonb_agg( + jsonb_build_object('column', ec.column_key, + 'closes_at', ec.closes_at) + ORDER BY ec.column_key) + FROM v3.editable_columns_for(p_form_id, v_user_id, p_sheet, p_direction) ec), + '[]'::JSONB) + ); + + IF v_form_type = 'FORM_1' THEN + + IF p_sheet = 'SMETA' THEN + RETURN QUERY + SELECT + sm.row_type, + sm.depth, + ROW_NUMBER() OVER (ORDER BY sm.section_code) AS sort_order, + jsonb_build_object( + 'section_code', sm.section_code, + 'name', sm.name, + 'plan', jsonb_build_object( + 'support', jsonb_build_object('q1', sm.supp_plan_q1, 'q2', sm.supp_plan_q2, + 'q3', sm.supp_plan_q3, 'q4', sm.supp_plan_q4, + 'year', sm.supp_plan_year), + 'development', jsonb_build_object('q1', sm.dev_plan_q1, 'q2', sm.dev_plan_q2, + 'q3', sm.dev_plan_q3, 'q4', sm.dev_plan_q4, + 'year', sm.dev_plan_year), + 'total_year', sm.total_plan_year), + 'approved', jsonb_build_object( + 'support', jsonb_build_object('q1', sm.supp_appr_q1, 'q2', sm.supp_appr_q2, + 'q3', sm.supp_appr_q3, 'q4', sm.supp_appr_q4, + 'year', sm.supp_appr_year), + 'development', jsonb_build_object('q1', sm.dev_appr_q1, 'q2', sm.dev_appr_q2, + 'q3', sm.dev_appr_q3, 'q4', sm.dev_appr_q4, + 'year', sm.dev_appr_year), + 'total_year', sm.total_appr_year), + 'fact', jsonb_build_object( + 'support', jsonb_build_object('q1', sm.supp_act_q1, 'q2', sm.supp_act_q2, + 'q3', sm.supp_act_q3, 'q4', sm.supp_act_q4, + 'year', sm.supp_act_year), + 'development', jsonb_build_object('q1', sm.dev_act_q1, 'q2', sm.dev_act_q2, + 'q3', sm.dev_act_q3, 'q4', sm.dev_act_q4, + 'year', sm.dev_act_year), + 'total_year', sm.total_act_year), + 'corrected', jsonb_build_object( + 'support', jsonb_build_object('q2', sm.supp_corr_q2, 'q3', sm.supp_corr_q3, 'q4', sm.supp_corr_q4), + 'development', jsonb_build_object('q2', sm.dev_corr_q2, 'q3', sm.dev_corr_q3, 'q4', sm.dev_corr_q4)) + ) + FROM v3.v_form1_smeta(v_year, v_org_unit_id) sm + ORDER BY sm.section_code; + RETURN; + END IF; + + IF p_sheet IN ('AHR','CAP','OPER') THEN + RETURN QUERY + SELECT + j.row_type, + j.depth, + ROW_NUMBER() OVER (ORDER BY j._sort_path) AS sort_order, + jsonb_build_object( + 'line_id', j.line_id, + 'header', j.header, + 'plan', j.plan_data, + 'contract_summary', j.contract_summary, + 'allocation', j.allocation_data, + 'sequestration', j.sequestration_data, + 'reserve', j.reserve_data, + 'approved', j.approved_data, + 'collegial', j.collegial_data, + 'ckk', j.ckk_data, + 'contract_detail', j.contract_detail, + 'q1', j.q1_data, + 'q2', j.q2_data, + 'q3', j.q3_data, + 'q4', j.q4_data, + 'totals', j.totals_data + ) + FROM v3.v_form1_sheet_jsonb(p_form_id, p_sheet, p_direction, p_sections) j + ORDER BY j._sort_path; + RETURN; + END IF; + + RAISE EXCEPTION 'Unknown FORM_1 sheet: %', p_sheet + USING HINT = 'Use AHR / CAP / OPER / SMETA'; + END IF; + + IF v_form_type = 'FORM_2' THEN + RETURN QUERY SELECT * FROM v3.v_form2_view(p_form_id, p_sheet, p_sections); + RETURN; + END IF; + + IF v_form_type = 'FORM_4' THEN + RETURN QUERY SELECT * FROM v3.v_form4_view(p_form_id, p_sheet, p_sections); + RETURN; + END IF; + + RAISE EXCEPTION 'Unsupported form_type=% for form_id=%', v_form_type, p_form_id; + +END; +$function$ +; + +CREATE OR REPLACE FUNCTION v3._form_affected_rows(p_form_id integer, p_sheet character varying, p_ids integer[], p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[]) + RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) + LANGUAGE plpgsql +AS $function$ +DECLARE + v_form_type VARCHAR; + v_strip TEXT[] := ARRAY[]::TEXT[]; + v_lc_path INT[]; + v_eid_set INT[]; + v_ei_path INT[]; +BEGIN + SELECT bf.form_type_code INTO v_form_type + FROM v3.budget_form bf + WHERE bf.id = p_form_id; + + IF p_sections IS NOT NULL THEN + SELECT COALESCE(array_agg(k), ARRAY[]::TEXT[]) + INTO v_strip + FROM unnest(ARRAY['plan','seq_dfip','seq_ssp','approved','contract','booking', + 'contract_summary','allocation','collegial','ckk', + 'sequestration','reserve','contract_detail', + 'q1','q2','q3','q4','totals', + 'fact_q1','fact_q2','fact_q3','fact_q4']) AS k + WHERE NOT (k = ANY(p_sections)); + END IF; + + IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN + RETURN QUERY + SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip + FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v + WHERE v.row_type = 'INPUT' AND (v.data->>'id')::INT = ANY(p_ids) + ORDER BY v.sort_order; + + ELSIF p_sheet = 'AHR_LIMIT' THEN + v_lc_path := v3._limit_template_path_set(p_ids); + RETURN QUERY + SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip + FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v + WHERE (v.data->>'id')::INT = ANY(v_lc_path) + ORDER BY v.sort_order; + + ELSIF p_sheet = 'OTCH9F' THEN + RETURN QUERY + SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip + FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v + WHERE EXISTS ( + SELECT 1 FROM v3.fixed_asset_report far + WHERE far.id = ANY(p_ids) + AND far.expense_item_id = (v.data->>'expense_item_id')::INT + AND far.equipment_name IS NOT DISTINCT FROM (v.data->>'equipment_name')) + ORDER BY v.sort_order; + + ELSE + SELECT array_agg(DISTINCT bl.expense_item_id) + INTO v_eid_set + FROM v3.budget_line bl + WHERE bl.id = ANY(p_ids); + v_ei_path := v3._expense_item_path_set(v_eid_set); + + IF v_form_type = 'FORM_4' THEN + RETURN QUERY + WITH vv AS MATERIALIZED ( + SELECT f.row_type, f.depth, f.sort_order, f.data + FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) f + WHERE f.row_type <> 'META' + ), + chg AS ( + SELECT DISTINCT (x.data->'header'->>'program_id')::INT AS program_id, + (x.data->'header'->>'project_id')::INT AS project_id + FROM vv x + WHERE x.row_type = 'INPUT' AND (x.data->>'line_id')::INT = ANY(p_ids) + ) + SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip + FROM vv v + WHERE (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = ANY(p_ids)) + OR (v.row_type = 'ROOT' + AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)) + OR (v.row_type = 'GROUP' + AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path) + AND (v.data->'header'->>'program_id')::INT IN (SELECT c.program_id FROM chg c)) + OR (v.row_type = 'ITEM' + AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path) + AND (v.data->'header'->>'project_id')::INT IN (SELECT c.project_id FROM chg c)) + ORDER BY v.sort_order; + ELSE + RETURN QUERY + SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip + FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v + WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM','SUB_SUB_ITEM') + AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)) + OR (v.row_type = 'INPUT' + AND (v.data->>'line_id')::INT = ANY(p_ids)) + ORDER BY v.sort_order; + END IF; + END IF; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION v3.get_form2_ahr_satellite_delta(p_form_id integer, p_source_sheet character varying, p_source_row_id integer) + RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) + LANGUAGE sql + STABLE +AS $function$ + WITH target_lines AS ( + SELECT m.target_line_id AS line_id + FROM v3.form2_ahr_satellite_line_source m + JOIN v3.budget_line bl ON bl.id = m.target_line_id + WHERE m.source_sheet = p_source_sheet + AND m.source_id = p_source_row_id + AND bl.budget_form_id = p_form_id + AND p_source_sheet IN ('AHR_RENT', 'AHR_UTILITY', 'AHR_SECURITY') + + UNION + + SELECT bl.id AS line_id + FROM v3.limit_template lt + JOIN v3.expense_item ei + ON ei.sheet = 'AHR' + AND ei.direction = 'Support' + AND ( + (ei.depth = 3 AND ei.item_id = lt.expense_item_code) + OR ei.num_group_id = lt.expense_item_code + ) + JOIN v3.budget_line bl + ON bl.budget_form_id = p_form_id + AND bl.expense_item_id = ei.id + AND bl.name = regexp_replace( + lt.name, + '^[[:space:]]*-[[:space:]]*', + '' + ) + WHERE p_source_sheet = 'AHR_LIMIT' + AND lt.id = p_source_row_id + AND lt.row_type = 'LEAF' + ), + target_path AS ( + SELECT unnest( + v3._expense_item_path_set( + COALESCE(array_agg(bl.expense_item_id), ARRAY[]::integer[]) + ) + ) AS expense_item_id + FROM target_lines tl + JOIN v3.budget_line bl ON bl.id = tl.line_id + ) + SELECT v.row_type, v.depth, v.sort_order, v.data + FROM v3.v_form_view(p_form_id, 'AHR', NULL, 'Support') v + WHERE ( + v.row_type IN ('ROOT', 'GROUP', 'ITEM', 'SUB_ITEM') + AND (v.data->'header'->>'expense_item_id')::INT IN ( + SELECT expense_item_id FROM target_path + ) + ) + OR ( + v.row_type = 'INPUT' + AND (v.data->>'line_id')::INT IN ( + SELECT line_id FROM target_lines + ) + ) + ORDER BY v.sort_order; +$function$ +; -- 2.47.2 From 42d47a4c1fd19f69438b6fa1c086ee500cb9907c Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 21:41:38 +0400 Subject: [PATCH 06/20] =?UTF-8?q?test(sheet):=20=D1=8D=D0=BA=D0=B2=D0=B8?= =?UTF-8?q?=D0=B2=D0=B0=D0=BB=D0=B5=D0=BD=D1=82=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=B0=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D1=83=20=D0=B8=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D0=BD=D0=BE=D0=B9=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B8?= =?UTF-8?q?=20=E2=80=94=20=D0=BA=D1=80=D0=B0=D1=81=D0=BD=D1=8B=D0=B5=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D1=8B=20(AURORA-1850)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- api/tests/integration/sheet_contract.py | 105 +++++ .../test_upd_form_cells_subtree.py | 412 ++++++++++++++++++ 2 files changed, 517 insertions(+) create mode 100644 api/tests/integration/test_upd_form_cells_subtree.py diff --git a/api/tests/integration/sheet_contract.py b/api/tests/integration/sheet_contract.py index c239c39..acd55d4 100644 --- a/api/tests/integration/sheet_contract.py +++ b/api/tests/integration/sheet_contract.py @@ -20,3 +20,108 @@ def assert_patch_rows_match_sheet(patch_rows: list[dict], sheet_rows: list[dict] key = row_key(row) assert key in sheet_by_key, f"строки {key} нет в GET /sheet" assert row["data"] == sheet_by_key[key]["data"], f"data строки {key} расходится с GET" + + +# --- AURORA-1850: эталон (фильтр 0050 поверх полного v_form_view) и хелперы SQL --- + +Row4 = tuple # (row_type, depth, sort_order, data::text) + +_STRIP_ALL = [ + "plan", "seq_dfip", "seq_ssp", "approved", "contract", "booking", + "contract_summary", "allocation", "collegial", "ckk", + "sequestration", "reserve", "contract_detail", + "q1", "q2", "q3", "q4", "totals", + "fact_q1", "fact_q2", "fact_q3", "fact_q4", +] + +_SAT_SHEETS = ("AHR_RENT", "AHR_UTILITY", "AHR_SECURITY") + + +def strip_keys(sections: list[str] | None) -> list[str]: + """Ключи верхнего уровня исключённых разделов (как v_strip в 0050).""" + if sections is None: + return [] + return [k for k in _STRIP_ALL if k not in sections] + + +def sheet_kind(sheet: str) -> str: + if sheet in _SAT_SHEETS: + return "sat" + if sheet == "AHR_LIMIT": + return "limit" + if sheet == "OTCH9F": + return "otch9f" + return "tree" + + +_REF_FROM = """ + SELECT v.row_type, v.depth, v.sort_order, (v.data - CAST(:strip AS text[]))::text + FROM v3.v_form_view(:f, :s, CAST(:sec AS text[]), CAST(:d AS varchar)) v + WHERE {where} + ORDER BY v.sort_order""" + +_REF_WHERE = { + "sat": "v.row_type = 'INPUT' AND (v.data->>'id')::int = ANY(CAST(:ids AS int[]))", + "limit": "(v.data->>'id')::int = ANY(v3._limit_template_path_set(CAST(:ids AS int[])))", + "otch9f": """EXISTS (SELECT 1 FROM v3.fixed_asset_report far + WHERE far.id = ANY(CAST(:ids AS int[])) + AND far.expense_item_id = (v.data->>'expense_item_id')::int + AND far.equipment_name IS NOT DISTINCT FROM (v.data->>'equipment_name'))""", + "tree": """(v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM','SUB_SUB_ITEM') + AND (v.data->'header'->>'expense_item_id')::int = ANY(v3._expense_item_path_set( + ARRAY(SELECT DISTINCT bl.expense_item_id FROM v3.budget_line bl + WHERE bl.id = ANY(CAST(:ids AS int[])))))) + OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::int = ANY(CAST(:ids AS int[])))""", +} + + +def _rows(db_exec, sql: str, **params) -> list: + return [tuple(r) for r in db_exec(sql, **params)] + + +def reference_affected_rows(db_exec, form_id, sheet, ids, direction=None, sections=None, strip=True): + sql = _REF_FROM.format(where=_REF_WHERE[sheet_kind(sheet)]) + return _rows( + db_exec, sql, f=form_id, s=sheet, ids=list(ids), d=direction, sec=sections, + strip=strip_keys(sections) if strip else [], + ) + + +def affected_rows(db_exec, form_id, sheet, ids, direction=None, sections=None): + return _rows( + db_exec, + """SELECT r.row_type, r.depth, r.sort_order, r.data::text + FROM v3._form_affected_rows(:f, :s, CAST(:ids AS int[]), CAST(:d AS varchar), + CAST(:sec AS text[])) r + ORDER BY r.sort_order""", + f=form_id, s=sheet, ids=list(ids), d=direction, sec=sections, + ) + + +def focus_builder_rows(db_exec, form_type, form_id, sheet, ids, direction=None, sections=None): + if form_type == "FORM_2": + sql = """SELECT r.row_type, r.depth, r.sort_order, r.data::text + FROM v3.v_form2_view(:f, :s, CAST(:sec AS text[]), CAST(:ids AS int[])) r + ORDER BY r.sort_order""" + params = dict(f=form_id, s=sheet, sec=sections, ids=list(ids)) + else: + sql = """SELECT r.row_type, r.depth, r.sort_order, r.data::text + FROM v3.v_form1_view(:f, :s, CAST(:sec AS text[]), CAST(:d AS varchar), + CAST(:ids AS int[])) r + ORDER BY r.sort_order""" + params = dict(f=form_id, s=sheet, sec=sections, d=direction, ids=list(ids)) + return _rows(db_exec, sql, **params) + + +def assert_rows_equal(actual: list, expected: list, label: str) -> None: + """Построчное сравнение; сообщение называет первую отличающуюся строку и длины.""" + if actual == expected: + return + n = min(len(actual), len(expected)) + first = next((i for i in range(n) if actual[i] != expected[i]), n) + a = actual[first] if first < len(actual) else None + e = expected[first] if first < len(expected) else None + raise AssertionError( + f"{label}: длины actual={len(actual)} expected={len(expected)}; " + f"первое расхождение в позиции {first}:\n actual = {a}\n expected = {e}" + ) diff --git a/api/tests/integration/test_upd_form_cells_subtree.py b/api/tests/integration/test_upd_form_cells_subtree.py new file mode 100644 index 0000000..19621ee --- /dev/null +++ b/api/tests/integration/test_upd_form_cells_subtree.py @@ -0,0 +1,412 @@ +"""AURORA-1850: эквивалентность пересчёта поддерева и полной сборки листа. + +Эталон — фильтр 0050 поверх полного v3.v_form_view. Новый путь (v3._form_affected_rows и +сборщики v3.v_form2_view / v3.v_form1_view с фокусом по id) обязан давать те же строки. +""" + +import uuid + +import pytest + +from sheet_contract import ( + affected_rows, + assert_patch_rows_match_sheet, + assert_rows_equal, + focus_builder_rows, + reference_affected_rows, + row_key, +) +from test_upd_form_cells_full_rows import ( # noqa: F401 (фикстуры и хелперы) + API, + _add_line, + _get_sheet, + _new_form, + _patch_cell, + _qs, + db_exec, + headers, +) + +TREE_TYPES = ("ROOT", "GROUP", "ITEM", "SUB_ITEM", "SUB_SUB_ITEM") +SATELLITES = ("AHR_RENT", "AHR_UTILITY", "AHR_SECURITY") +FORM2_SHEETS = ("AHR", "CAP", "OPER", "AHR_RENT", "AHR_UTILITY", "AHR_SECURITY", "AHR_LIMIT", "OTCH9F") + +# (form_type, sheet, direction, sections, год новой формы) +CASES = { + "F1-AHR-Support": ("FORM_1", "AHR", "Support", None, 2041), + "F1-CAP-Support": ("FORM_1", "CAP", "Support", None, 2043), + "F1-OPER": ("FORM_1", "OPER", None, None, 2044), + "F2-AHR": ("FORM_2", "AHR", None, None, 2045), + "F2-CAP": ("FORM_2", "CAP", None, None, 2046), + "F2-OPER": ("FORM_2", "OPER", None, None, 2047), + "F2-AHR_RENT": ("FORM_2", "AHR_RENT", None, None, 0), + "F2-AHR_UTILITY": ("FORM_2", "AHR_UTILITY", None, None, 0), + "F2-AHR_SECURITY": ("FORM_2", "AHR_SECURITY", None, None, 0), + "F2-AHR_LIMIT": ("FORM_2", "AHR_LIMIT", None, None, 0), + "F2-OTCH9F": ("FORM_2", "OTCH9F", None, None, 0), + "F2-AHR-plan": ("FORM_2", "AHR", None, ["plan"], 2048), +} +SAT_FORM = 4 + + +def _ordered_leaves(db_exec, form_type, sheet): + """Листовые статьи, привязанные к типу формы, в порядке mv_expense_item_tree.path.""" + return [ + (r[0], r[1], list(r[2])) + for r in db_exec( + """ + SELECT ei.id, ei.parent_id, t.path + FROM v3.expense_item ei + JOIN v3.mv_expense_item_tree t ON t.id = ei.id + JOIN v3.expense_item_form_type ft ON ft.expense_item_id = ei.id AND ft.form_type_code = :ft + WHERE ei.sheet = :sheet AND ei.direction = 'Support' + AND NOT EXISTS (SELECT 1 FROM v3.expense_item c WHERE c.parent_id = ei.id) + ORDER BY t.path + """, + ft=form_type, sheet=sheet, + ) + ] + + +def _pick_items(db_exec, form_type, sheet): + """первая, последняя, две сестры, строка из другого ROOT (без повторов).""" + leaves = _ordered_leaves(db_exec, form_type, sheet) + first, last = leaves[0], leaves[-1] + chosen = [first, last] + by_parent = {} + for lf in leaves: + by_parent.setdefault(lf[1], []).append(lf) + sib = next( + (g for g in by_parent.values() if len(g) >= 2 and all(x not in chosen for x in g[:2])), None + ) + sibs = sib[:2] if sib else [] + other = next( + (lf for lf in leaves if lf[2][0] not in (first[2][0], last[2][0]) and lf not in chosen + sibs), + None, + ) + items = [first[0], last[0]] + [s[0] for s in sibs] + ([other[0]] if other else []) + return items, {"first": first[0], "last": last[0], "sibs": [s[0] for s in sibs], + "other": other[0] if other else None} + + +def _input_line_ids(rows): + return [r["data"]["line_id"] for r in rows if r["row_type"] == "INPUT"] + + +def _prepare(client, headers, db_exec, case): + """Создаёт данные листа. Возвращает (form_id, units, batches).""" + form_type, sheet, direction, sections, year = CASES[case] + kind = "tree" if sheet in ("AHR", "CAP", "OPER") else sheet + if kind == "tree": + form_id = _new_form(client, headers, form_type, year) + items, names = _pick_items(db_exec, form_type, sheet) + lines = [] + for n, item in enumerate(items): + lines.append(_add_line(client, headers, form_id, sheet, item, direction)) + for n, line in enumerate(lines): + _patch_cell(client, headers, form_id, sheet, line, "plan.q1", 10 + n, direction) + _patch_cell(client, headers, form_id, sheet, lines[0], "header.justification", "обоснование", direction) + batch = list(lines) + if case == "F1-AHR-Support": + dev = _add_line(client, headers, form_id, sheet, items[0], "Development") + _patch_cell(client, headers, form_id, sheet, dev, "plan.q1", 77, "Development") + batch.append(dev) + get_rows = _get_sheet(client, headers, form_id, sheet, direction) + units = _input_line_ids(get_rows) + assert set(lines) <= set(units) + return form_id, units, [batch, batch + [lines[0]]] + if sheet in SATELLITES: + ids = [] + for n in range(3): + r = client.post( + f"{API}/{SAT_FORM}/sheet/{sheet}/line", + json={"vsp_id": 1 + n % 3, "contract_number": f"C-{uuid.uuid4().hex[:6]}"}, + headers=headers, + ) + assert r.status_code == 200, r.text + sid = r.json()["result"][0]["data"]["id"] + _patch_cell(client, headers, SAT_FORM, sheet, sid, "plan.q1", 5 + n) + ids.append(sid) + get_rows = _get_sheet(client, headers, SAT_FORM, sheet) + units = [r["data"]["id"] for r in get_rows if r["row_type"] == "INPUT"] + assert set(ids) <= set(units) + return SAT_FORM, units, [ids, ids + [ids[0]]] + if sheet == "AHR_LIMIT": + extra = [r[0] for r in db_exec( + "SELECT id FROM v3.limit_template WHERE row_type = 'LEAF' AND id <> 13 ORDER BY id LIMIT 2")] + ids = [13] + extra + for n, i in enumerate(ids): + _patch_cell(client, headers, SAT_FORM, sheet, i, "qty_q1", 3 + n) + units = [r[0] for r in db_exec("SELECT id FROM v3.limit_template WHERE row_type = 'LEAF' ORDER BY id")] + return SAT_FORM, units, [ids, ids + [ids[0]]] + if sheet == "OTCH9F": + return SAT_FORM, *_prepare_otch9f(client, headers, db_exec) + raise AssertionError(case) + + +def _prepare_otch9f(client, headers, db_exec): + items = [r[0] for r in db_exec( + """SELECT ei.id FROM v3.expense_item ei + WHERE ei.sheet = 'AHR' + AND NOT EXISTS (SELECT 1 FROM v3.expense_item c WHERE c.parent_id = ei.id) + AND EXISTS (SELECT 1 FROM v3.expense_item_form_type t + WHERE t.expense_item_id = ei.id AND t.form_type_code = 'FORM_2') + AND NOT EXISTS (SELECT 1 FROM v3.fixed_asset_report far + WHERE far.budget_form_id = :f AND far.expense_item_id = ei.id) + ORDER BY ei.id LIMIT 2""", f=SAT_FORM)] + layout = [(items[0], "E1", 1), (items[0], "E1", 2), (items[0], "E1", 3), (items[0], "E2", 1), + (items[0], None, 1), (items[0], None, 2), (items[1], "E1", 1)] + ids = [] + for item, name, month in layout: + ids.append(db_exec( + """INSERT INTO v3.fixed_asset_report + (budget_form_id, expense_item_id, equipment_name, month, + opening_qty_604, opening_amt_604, acquired_qty_604, acquired_amt_604, + disposed_qty_604, disposed_amt_604, + opening_qty_60415, opening_amt_60415, acquired_qty_60415, acquired_amt_60415) + VALUES (:f, :i, :n, :m, 10, 1000, 2, 200, 1, 100, 5, 500, 1, 50) RETURNING id""", + f=SAT_FORM, i=item, n=name, m=month)[0][0]) + return ids, [ids, ids + [ids[0]]] + + +def _check(db_exec, path, form_type, form_id, sheet, ids, direction, sections, label): + if path == "affected": + actual = affected_rows(db_exec, form_id, sheet, ids, direction, sections) + expected = reference_affected_rows(db_exec, form_id, sheet, ids, direction, sections, strip=True) + else: + actual = focus_builder_rows(db_exec, form_type, form_id, sheet, ids, direction, sections) + expected = reference_affected_rows(db_exec, form_id, sheet, ids, direction, sections, strip=False) + assert_rows_equal(actual, expected, label) + + +@pytest.mark.parametrize("path", ["affected", "builder"]) +@pytest.mark.parametrize("case", [c for c in CASES]) +def test_affected_rows_match_reference(client, headers, db_exec, path, case): + form_type, sheet, direction, sections, _ = CASES[case] + form_id, units, batches = _prepare(client, headers, db_exec, case) + assert units, "в листе нет строк" + for i in units: + _check(db_exec, path, form_type, form_id, sheet, [i], direction, sections, f"{case} id={i}") + for n, batch in enumerate(batches): + _check(db_exec, path, form_type, form_id, sheet, batch, direction, sections, f"{case} batch#{n}") + + +EDIT_CASES = ["F1-AHR-Support", "F2-AHR", "F2-AHR_RENT", "F2-AHR_LIMIT", "F2-OTCH9F", "delta"] +VF_SIG = "v3.v_form_view(integer,character varying,text[],character varying,integer)" +_CALLS = "pg_stat_get_xact_function_calls(CAST(CAST('" + VF_SIG + "' AS regprocedure) AS oid))" + + +@pytest.mark.parametrize("case", EDIT_CASES) +def test_edit_path_does_not_call_v_form_view(client, headers, db_exec, case): + # R-2: роль perf — суперпользователь, track_functions доступен; тест не пропускается. + assert db_exec("SELECT rolsuper FROM pg_roles WHERE rolname = current_user")[0][0] is True, ( + "для track_functions нужна суперпользовательская роль" + ) + if case == "delta": + r = client.post( + f"{API}/{SAT_FORM}/sheet/AHR_RENT/line", + json={"vsp_id": 1, "contract_number": f"C-{uuid.uuid4().hex[:6]}"}, + headers=headers, + ) + assert r.status_code == 200, r.text + rent_id = r.json()["result"][0]["data"]["id"] + _patch_cell(client, headers, SAT_FORM, "AHR_RENT", rent_id, "plan.q1", 100) + call, params = ("SELECT count(*) FROM v3.get_form2_ahr_satellite_delta(4, 'AHR_RENT', :i)"), {"i": rent_id} + else: + form_type, sheet, direction, sections, _ = CASES[case] + form_id, units, batches = _prepare(client, headers, db_exec, case) + call = ("SELECT count(*) FROM v3._form_affected_rows(:f, :s, CAST(:ids AS int[]), " + "CAST(:d AS varchar), CAST(:sec AS text[]))") + params = dict(f=form_id, s=sheet, ids=batches[0], d=direction, sec=sections) + db_exec("SET LOCAL track_functions = 'all'") + n0 = db_exec(f"SELECT COALESCE({_CALLS}, 0)")[0][0] + db_exec(call, **params) + n1 = db_exec(f"SELECT COALESCE({_CALLS}, 0)")[0][0] + assert n1 - n0 == 0, f"v_form_view вызван {n1 - n0} раз на пути правки" + + +def test_empty_focus_returns_nothing(db_exec): + for s in FORM2_SHEETS: + rows = db_exec( + "SELECT * FROM v3.v_form2_view(4, :s, CAST(NULL AS text[]), CAST('{}' AS int[]))", s=s) + assert rows == [], f"v_form2_view пуст для {s}: {len(rows)}" + rows = db_exec( + "SELECT * FROM v3.v_form1_view(1, 'AHR', CAST(NULL AS text[]), 'Support', CAST('{}' AS int[]))") + assert rows == [] + assert db_exec("SELECT * FROM v3._form_affected_rows(4, 'AHR', CAST('{}' AS int[]))") == [] + assert db_exec("SELECT * FROM v3.get_form2_ahr_satellite_delta(4, 'AHR_RENT', -1)") == [] + + +def _patch_cells(client, headers, form_id, sheet, changes, direction=None): + r = client.patch( + f"{API}/{form_id}/sheet/{sheet}/cells{_qs(direction)}", json={"changes": changes}, headers=headers) + assert r.status_code == 200, r.text + return r.json()["result"] + + +def test_patch_cells_two_branches_matches_get(client, headers, db_exec): + form_id = _new_form(client, headers, "FORM_2", 2054) + items, names = _pick_items(db_exec, "FORM_2", "AHR") + l1 = _add_line(client, headers, form_id, "AHR", names["first"]) + l2 = _add_line(client, headers, form_id, "AHR", names["last"]) + patch = _patch_cells(client, headers, form_id, "AHR", [ + {"line_id": l1, "column": "plan.q1", "value": 11}, + {"line_id": l2, "column": "plan.q1", "value": 22}, + ]) + get_rows = _get_sheet(client, headers, form_id, "AHR") + assert_patch_rows_match_sheet(patch, get_rows) + assert {r["data"]["line_id"] for r in patch if r["row_type"] == "INPUT"} == {l1, l2} + keys = [row_key(r) for r in patch] + assert len(keys) == len(set(keys)) + + +@pytest.mark.parametrize( + "form_type,direction,year", + [("FORM_2", None, 2055), ("FORM_1", "Support", 2056)], + ids=["FORM_2-AHR", "FORM_1-AHR-Support"], +) +def test_patch_after_add_and_delete_matches_get(client, headers, db_exec, form_type, direction, year): + form_id = _new_form(client, headers, form_type, year) + leaves = _ordered_leaves(db_exec, form_type, "AHR") + step = max(len(leaves) // 5, 1) + items = [leaves[i * step][0] for i in range(5)] + lines = [_add_line(client, headers, form_id, "AHR", it, direction) for it in items] + get0 = _get_sheet(client, headers, form_id, "AHR", direction) + order = [r["data"]["line_id"] for r in get0 if r["row_type"] == "INPUT" and r["data"]["line_id"] in lines] + victim, below = order[2], order[3] + r = client.delete(f"{API}/{form_id}/sheet/AHR/line/{victim}{_qs(direction)}", headers=headers) + assert r.status_code == 200, r.text + patch = _patch_cell(client, headers, form_id, "AHR", below, "plan.q1", 42, direction) + get_rows = _get_sheet(client, headers, form_id, "AHR", direction) + assert_patch_rows_match_sheet(patch, get_rows) + + def ident(row): + if row["row_type"] == "INPUT": + return ("L", row["data"]["line_id"]) + return ("N", row["data"]["header"]["expense_item_id"], row["row_type"]) + + by_ident = {ident(r): r["sort_order"] for r in get_rows if r["row_type"] != "META"} + for row in patch: + assert row["sort_order"] == by_ident[ident(row)], ident(row) + + +def test_patch_form1_development_matches_get(client, headers, db_exec): + form_id = _new_form(client, headers, "FORM_1", 2057) + items, names = _pick_items(db_exec, "FORM_1", "AHR") + sup = _add_line(client, headers, form_id, "AHR", names["first"], "Support") + dev1 = _add_line(client, headers, form_id, "AHR", names["first"], "Development") + dev2 = _add_line(client, headers, form_id, "AHR", names["last"], "Development") + _patch_cell(client, headers, form_id, "AHR", sup, "plan.q1", 5, "Support") + _patch_cell(client, headers, form_id, "AHR", dev2, "plan.q1", 6, "Development") + patch = _patch_cell(client, headers, form_id, "AHR", dev1, "plan.q1", 7, "Development") + assert_patch_rows_match_sheet(patch, _get_sheet(client, headers, form_id, "AHR", "Development")) + assert {r["data"]["line_id"] for r in patch if r["row_type"] == "INPUT"} == {dev1} + + +def test_patch_otch9f_returns_all_months_of_equipment(client, headers, db_exec): + ids, _ = _prepare_otch9f(client, headers, db_exec) + e1_m1 = ids[0] + item = db_exec("SELECT expense_item_id FROM v3.fixed_asset_report WHERE id = :i", i=e1_m1)[0][0] + before = _get_sheet(client, headers, SAT_FORM, "OTCH9F") + patch = _patch_cell(client, headers, SAT_FORM, "OTCH9F", e1_m1, "b_604.acquired_qty", 9) + get_rows = _get_sheet(client, headers, SAT_FORM, "OTCH9F") + assert_patch_rows_match_sheet(patch, get_rows) + + def of_equipment(rows): + return {row_key(r): r for r in rows + if r["row_type"] != "META" and r["data"].get("expense_item_id") == item + and r["data"].get("equipment_name") == "E1"} + + expected = of_equipment(get_rows) + assert len(expected) >= 3 + assert {row_key(r) for r in patch} == set(expected) + prev = of_equipment(before) + changed = [k for k in expected if expected[k]["data"] != prev[k]["data"]] + assert len(changed) >= 2, "остатки следующих месяцев не пересчитаны" + + +def _sort_key_candidate(db_exec): + rows = db_exec( + """ + SELECT b.id, b.parent_id + FROM v3.mv_expense_item_tree b + JOIN v3.mv_expense_item_tree a ON a.id = b.parent_id + JOIN v3.expense_item_form_type fb ON fb.expense_item_id = b.id AND fb.form_type_code = 'FORM_2' + JOIN v3.expense_item_form_type fa ON fa.expense_item_id = a.id AND fa.form_type_code = 'FORM_2' + WHERE b.sheet = 'AHR' AND a.depth = 3 AND b.depth = 4 + AND NOT EXISTS (SELECT 1 FROM v3.expense_item c WHERE c.parent_id = b.id) + AND NOT EXISTS (SELECT 1 FROM v3.budget_line bl WHERE bl.id = b.id) + ORDER BY b.id LIMIT 1 + """ + ) + return rows[0][0] if rows else None + + +def test_sort_key_tie_node_before_line(client, headers, db_exec): + child = _sort_key_candidate(db_exec) + if child is None: + pytest.skip("нет статьи AHR глубины 3 с дочерней глубины 4 и свободным id в budget_line") + form_id = _new_form(client, headers, "FORM_2", 2058) + db_exec( + "INSERT INTO v3.budget_line (id, budget_form_id, expense_item_id, name) " + "VALUES (:i, :f, :i, 'tie')", i=child, f=form_id) + g1 = _get_sheet(client, headers, form_id, "AHR") + g2 = _get_sheet(client, headers, form_id, "AHR") + assert [row_key(r) for r in g1] == [row_key(r) for r in g2] + idx = next(n for n, r in enumerate(g1) if r["row_type"] == "INPUT" and r["data"]["line_id"] == child) + prev = g1[idx - 1] + assert prev["row_type"] in TREE_TYPES and prev["data"]["header"]["expense_item_id"] == child, prev + patch = _patch_cell(client, headers, form_id, "AHR", child, "plan.q1", 8) + assert_patch_rows_match_sheet(patch, _get_sheet(client, headers, form_id, "AHR")) + + +def _delta_equals_get(db_exec, sheet, row_id): + delta = [tuple(r) for r in db_exec( + "SELECT row_type, depth, sort_order, data::text " + "FROM v3.get_form2_ahr_satellite_delta(4, :s, :i)", s=sheet, i=row_id)] + full = {r[2]: tuple(r) for r in db_exec( + "SELECT row_type, depth, sort_order, data::text " + "FROM v3.v_form_view(4, 'AHR', NULL, 'Support')")} + assert delta, "дельта пуста" + for row in delta: + assert row[2] in full, f"sort_order {row[2]} нет в полном AHR" + assert_rows_equal([row], [full[row[2]]], f"delta {sheet} sort_order={row[2]}") + return delta + + +def test_ahr_delta_matches_get_after_rent_edit(client, headers, db_exec): + r = client.post( + f"{API}/{SAT_FORM}/sheet/AHR_RENT/line", + json={"vsp_id": 1, "contract_number": f"C-{uuid.uuid4().hex[:6]}"}, + headers=headers, + ) + assert r.status_code == 200, r.text + rent_id = r.json()["result"][0]["data"]["id"] + _patch_cell(client, headers, SAT_FORM, "AHR_RENT", rent_id, "plan.q1", 100) + delta = _delta_equals_get(db_exec, "AHR_RENT", rent_id) + import json + assert any( + row[0] == "INPUT" and (json.loads(row[3]).get("plan") or {}).get("q1") == 100 for row in delta + ), "в дельте нет INPUT с plan.q1 = 100" + + +def test_ahr_delta_includes_sub_sub_item_for_depth4_limit(client, headers, db_exec): + rows = db_exec( + """ + SELECT lt.id, t.id + FROM v3.limit_template lt + JOIN v3.mv_expense_item_tree t ON t.num_group_id = lt.expense_item_code + JOIN v3.expense_item_form_type ft ON ft.expense_item_id = t.id AND ft.form_type_code = 'FORM_2' + WHERE lt.row_type = 'LEAF' AND t.sheet = 'AHR' AND t.depth = 4 + ORDER BY lt.id LIMIT 1 + """ + ) + if not rows: + pytest.skip("нет LEAF шаблона лимитов с кодом статьи AHR глубины 4") + lt_id, eid = rows[0] + _patch_cell(client, headers, SAT_FORM, "AHR_LIMIT", lt_id, "qty_q1", 4) + delta = _delta_equals_get(db_exec, "AHR_LIMIT", lt_id) + import json + assert any( + row[0] == "SUB_SUB_ITEM" and json.loads(row[3])["header"]["expense_item_id"] == eid for row in delta + ), "в дельте нет SUB_SUB_ITEM статьи глубины 4" -- 2.47.2 From 8a843b36605bcb947e45564ad3a7caff0068ad09 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 21:41:57 +0400 Subject: [PATCH 07/20] =?UTF-8?q?docs(openspec):=20aurora-1850=20=E2=80=94?= =?UTF-8?q?=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=81=D0=B5=D0=B2=D0=B0=20=D1=81=D0=BD=D0=B8=D0=BC=D0=BA=D0=B0?= =?UTF-8?q?=20GET=20(T1.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- openspec/changes/aurora-1850-upd-form-cells-subtree/design.md | 1 + 1 file changed, 1 insertion(+) diff --git a/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md b/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md index b778b2b..a127593 100644 --- a/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md +++ b/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md @@ -283,3 +283,4 @@ | 2026-10-28 | Контроллер (R-3): дополнительные БД `dfip_1850_base` и `dfip_1850_snap` в `perf-pg` одобрены; допустим только префикс `dfip_1850*`, БД `perf` не трогается | Быстрый сброс и снимок GET (см. решения плана) | Две лишние БД в контейнере | | 2026-10-28 | Контроллер (R-4): `before-0051.sql` коммитится в каталог change | База ревью и отката (D8) | Лишний файл в change | | 2026-10-28 | Контроллер (R-14): подпись коммитов `Co-Authored-By: Claude Opus 5.5` по плану и правилам волны 2, хотя системное напоминание сессии называет Sonnet 5.5 | Явная инструкция пользователя приоритетнее напоминания | Неверное имя модели в трейлере коммитов | +| 2026-10-28 | Исполнитель Task 1 (T1.2): посев снимка GET создаёт строки FORM_1 AHR/CAP и для Support, и для Development; снимок включает и фикстурные формы 1 и 4 | `expense_item.direction` везде Support, иначе ветка Development в снимке пуста | Снимок содержит чуть больше строк, чем описано в плане; на проверку корректности не влияет | -- 2.47.2 From b1fbac212e9feb9032516f03590c3f4f22d7712d Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 21:57:42 +0400 Subject: [PATCH 08/20] =?UTF-8?q?test(sheet):=20=D1=83=D1=81=D0=B8=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D1=80=D0=B0=D1=81=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=20=D1=8D=D0=BA?= =?UTF-8?q?=D0=B2=D0=B8=D0=B2=D0=B0=D0=BB=D0=B5=D0=BD=D1=82=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20=D0=BF=D0=BE=20=D1=80=D0=B5=D0=B2=D1=8C?= =?UTF-8?q?=D1=8E=20Task=202=20(AURORA-1850)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- api/tests/integration/sheet_contract.py | 2 +- .../test_upd_form_cells_subtree.py | 56 ++++++++++++++++--- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/api/tests/integration/sheet_contract.py b/api/tests/integration/sheet_contract.py index acd55d4..f93ea17 100644 --- a/api/tests/integration/sheet_contract.py +++ b/api/tests/integration/sheet_contract.py @@ -24,7 +24,7 @@ def assert_patch_rows_match_sheet(patch_rows: list[dict], sheet_rows: list[dict] # --- AURORA-1850: эталон (фильтр 0050 поверх полного v_form_view) и хелперы SQL --- -Row4 = tuple # (row_type, depth, sort_order, data::text) +Row4 = tuple[str, int, int, str] # (row_type, depth, sort_order, data::text) _STRIP_ALL = [ "plan", "seq_dfip", "seq_ssp", "approved", "contract", "booking", diff --git a/api/tests/integration/test_upd_form_cells_subtree.py b/api/tests/integration/test_upd_form_cells_subtree.py index 19621ee..faa850c 100644 --- a/api/tests/integration/test_upd_form_cells_subtree.py +++ b/api/tests/integration/test_upd_form_cells_subtree.py @@ -80,10 +80,15 @@ def _pick_items(db_exec, form_type, sheet): (g for g in by_parent.values() if len(g) >= 2 and all(x not in chosen for x in g[:2])), None ) sibs = sib[:2] if sib else [] - other = next( - (lf for lf in leaves if lf[2][0] not in (first[2][0], last[2][0]) and lf not in chosen + sibs), - None, - ) + assert len(sibs) == 2, "нет пары сестёр, отличных от первой/последней" + n_roots = len({lf[2][0] for lf in leaves}) + other = None + if n_roots >= 2: + assert first[2][0] != last[2][0], "первая и последняя статьи в одном ROOT при нескольких ROOT" + # строка из ROOT, отличного от ROOT сестёр + other = next((lf for lf in leaves + if lf[2][0] != sibs[0][2][0] and lf not in chosen + sibs), None) + assert other is not None, "нет строки из другого ROOT" items = [first[0], last[0]] + [s[0] for s in sibs] + ([other[0]] if other else []) return items, {"first": first[0], "last": last[0], "sibs": [s[0] for s in sibs], "other": other[0] if other else None} @@ -169,14 +174,25 @@ def _prepare_otch9f(client, headers, db_exec): return ids, [ids, ids + [ids[0]]] -def _check(db_exec, path, form_type, form_id, sheet, ids, direction, sections, label): +def _check(db_exec, path, form_type, form_id, sheet, ids, direction, sections, label, must_have_input=False): if path == "affected": actual = affected_rows(db_exec, form_id, sheet, ids, direction, sections) expected = reference_affected_rows(db_exec, form_id, sheet, ids, direction, sections, strip=True) else: actual = focus_builder_rows(db_exec, form_type, form_id, sheet, ids, direction, sections) expected = reference_affected_rows(db_exec, form_id, sheet, ids, direction, sections, strip=False) + assert expected, f"{label}: empty reference" assert_rows_equal(actual, expected, label) + if must_have_input: + assert any(_has_input(r, ids[0]) for r in actual), f"{label}: нет INPUT id={ids[0]} в результате" + + +def _has_input(row, i): + import json + if row[0] != "INPUT": + return False + d = json.loads(row[3]) + return d.get("line_id") == i or d.get("id") == i @pytest.mark.parametrize("path", ["affected", "builder"]) @@ -185,8 +201,15 @@ def test_affected_rows_match_reference(client, headers, db_exec, path, case): form_type, sheet, direction, sections, _ = CASES[case] form_id, units, batches = _prepare(client, headers, db_exec, case) assert units, "в листе нет строк" + is_tree = sheet in ("AHR", "CAP", "OPER") for i in units: - _check(db_exec, path, form_type, form_id, sheet, [i], direction, sections, f"{case} id={i}") + _check(db_exec, path, form_type, form_id, sheet, [i], direction, sections, f"{case} id={i}", + must_have_input=sheet != "AHR_LIMIT" and sheet != "OTCH9F") + if case == "F1-AHR-Support" and path == "affected": + dev = batches[0][-1] + rows = affected_rows(db_exec, form_id, sheet, [dev], direction, sections) + assert not any(_has_input(r, dev) for r in rows), "Development INPUT попал в результат Support" + del is_tree for n, batch in enumerate(batches): _check(db_exec, path, form_type, form_id, sheet, batch, direction, sections, f"{case} batch#{n}") @@ -219,7 +242,11 @@ def test_edit_path_does_not_call_v_form_view(client, headers, db_exec, case): "CAST(:d AS varchar), CAST(:sec AS text[]))") params = dict(f=form_id, s=sheet, ids=batches[0], d=direction, sec=sections) db_exec("SET LOCAL track_functions = 'all'") - n0 = db_exec(f"SELECT COALESCE({_CALLS}, 0)")[0][0] + c0 = db_exec(f"SELECT COALESCE({_CALLS}, 0)")[0][0] + db_exec("SELECT count(*) FROM v3.v_form_view(4, 'AHR_RENT', NULL, NULL)") + c1 = db_exec(f"SELECT COALESCE({_CALLS}, 0)")[0][0] + assert c1 - c0 == 1, "положительный контроль: счётчик не отслеживает v_form_view" + n0 = c1 db_exec(call, **params) n1 = db_exec(f"SELECT COALESCE({_CALLS}, 0)")[0][0] assert n1 - n0 == 0, f"v_form_view вызван {n1 - n0} раз на пути правки" @@ -258,6 +285,13 @@ def test_patch_cells_two_branches_matches_get(client, headers, db_exec): assert {r["data"]["line_id"] for r in patch if r["row_type"] == "INPUT"} == {l1, l2} keys = [row_key(r) for r in patch] assert len(keys) == len(set(keys)) + roots = { + db_exec("SELECT path[1] FROM v3.mv_expense_item_tree WHERE id = :i", i=i)[0][0] + for i in (names["first"], names["last"]) + } + assert len(roots) == 2, "строки l1/l2 в одном ROOT" + root_eids = {r["data"]["header"]["expense_item_id"] for r in patch if r["row_type"] == "ROOT"} + assert len(root_eids) >= 2, root_eids @pytest.mark.parametrize( @@ -354,9 +388,15 @@ def test_sort_key_tie_node_before_line(client, headers, db_exec): g2 = _get_sheet(client, headers, form_id, "AHR") assert [row_key(r) for r in g1] == [row_key(r) for r in g2] idx = next(n for n, r in enumerate(g1) if r["row_type"] == "INPUT" and r["data"]["line_id"] == child) + assert idx > 0 prev = g1[idx - 1] assert prev["row_type"] in TREE_TYPES and prev["data"]["header"]["expense_item_id"] == child, prev patch = _patch_cell(client, headers, form_id, "AHR", child, "plan.q1", 8) + node = next((r for r in patch if r["row_type"] in TREE_TYPES + and r["data"]["header"]["expense_item_id"] == child), None) + assert node is not None, "узел дочерней статьи отсутствует в ответе" + line_row = next(r for r in patch if r["row_type"] == "INPUT" and r["data"]["line_id"] == child) + assert node["sort_order"] < line_row["sort_order"] assert_patch_rows_match_sheet(patch, _get_sheet(client, headers, form_id, "AHR")) @@ -368,6 +408,7 @@ def _delta_equals_get(db_exec, sheet, row_id): "SELECT row_type, depth, sort_order, data::text " "FROM v3.v_form_view(4, 'AHR', NULL, 'Support')")} assert delta, "дельта пуста" + assert len({r[2] for r in delta}) == len(delta), "sort_order в дельте не уникален" for row in delta: assert row[2] in full, f"sort_order {row[2]} нет в полном AHR" assert_rows_equal([row], [full[row[2]]], f"delta {sheet} sort_order={row[2]}") @@ -404,6 +445,7 @@ def test_ahr_delta_includes_sub_sub_item_for_depth4_limit(client, headers, db_ex if not rows: pytest.skip("нет LEAF шаблона лимитов с кодом статьи AHR глубины 4") lt_id, eid = rows[0] + assert lt_id == 13, f"ожидался LEAF 13, найден {lt_id}" _patch_cell(client, headers, SAT_FORM, "AHR_LIMIT", lt_id, "qty_q1", 4) delta = _delta_equals_get(db_exec, "AHR_LIMIT", lt_id) import json -- 2.47.2 From d1a6cc2fada889fb0cb9d16a599ad00afeaf5796 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 22:10:54 +0400 Subject: [PATCH 09/20] =?UTF-8?q?perf(sql):=20=D1=84=D0=BE=D0=BA=D1=83?= =?UTF-8?q?=D1=81=20=D0=B8=20=5Fsort=5Frank=20=D0=B2=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B8=D1=82=D0=B5=D0=BB=D1=8F=D1=85=20FORM?= =?UTF-8?q?=5F2=20AHR/CAP/OPER=20(AURORA-1850)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../versions/0051_upd_form_cells_subtree.py | 15 + .../sql/0051_upd_form_cells_subtree.sql | 1378 +++++++++++++++++ 2 files changed, 1393 insertions(+) create mode 100644 api/alembic/versions/0051_upd_form_cells_subtree.py create mode 100644 api/alembic/versions/sql/0051_upd_form_cells_subtree.sql diff --git a/api/alembic/versions/0051_upd_form_cells_subtree.py b/api/alembic/versions/0051_upd_form_cells_subtree.py new file mode 100644 index 0000000..25ed435 --- /dev/null +++ b/api/alembic/versions/0051_upd_form_cells_subtree.py @@ -0,0 +1,15 @@ +from sql_migration import run_sql_file_migration + + +revision = "0051" +down_revision = "0050" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + run_sql_file_migration("0051_upd_form_cells_subtree.sql") + + +def downgrade() -> None: + pass diff --git a/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql b/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql new file mode 100644 index 0000000..6d744a2 --- /dev/null +++ b/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql @@ -0,0 +1,1378 @@ +-- AURORA-1850 (change: openspec/changes/aurora-1850-upd-form-cells-subtree): правка ячейки пересчитывает только затронутое поддерево. +-- Параметр фокуса p_focus_lines (D1) и колонка _sort_rank (D2) в построителях FORM_2 AHR/CAP/OPER. +-- Тела — буквальные, сняты с БД после 0050 (D8, before-0051.sql); порядок операторов — D9. +-- DROP FORM_1 и остальные функции добавляются следующими задачами плана. + +DROP FUNCTION v3.v_form2_view(integer, character varying, text[]); +DROP FUNCTION v3.v_form2_sheet_jsonb(integer, character varying, text[]); +DROP FUNCTION v3.v_form2_sheet_sections(integer, character varying, text[]); + +CREATE OR REPLACE FUNCTION v3.v_form2_sheet_sections(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[], p_focus_lines integer[] DEFAULT NULL::integer[]) + RETURNS TABLE(row_type character varying, depth integer, line_id integer, col_section_code character varying, col_item_id character varying, col_num_group_id character varying, col_name character varying, col_vsp_id integer, col_vsp_address character varying, col_plan_q1 numeric, col_plan_q2 numeric, col_plan_q3 numeric, col_plan_q4 numeric, col_plan_year numeric, col_plan_comment character varying, col_seq_dfip_q1 numeric, col_seq_dfip_q2 numeric, col_seq_dfip_q3 numeric, col_seq_dfip_q4 numeric, col_seq_dfip_year numeric, col_seq_dfip_just character varying, col_seq_ssp_q1 numeric, col_seq_ssp_q2 numeric, col_seq_ssp_q3 numeric, col_seq_ssp_q4 numeric, col_seq_ssp_year numeric, col_seq_ssp_just character varying, col_appr_q1 numeric, col_appr_q2 numeric, col_appr_q3 numeric, col_appr_q4 numeric, col_appr_year numeric, col_cd_counterparty character varying, col_cd_reference character varying, col_cd_date date, col_cd_subject character varying, col_cd_currency character varying, col_cd_ceiling numeric, col_cd_vat_rate character varying, col_cd_deadline character varying, col_cd_scheme character varying, col_cd_act character varying, col_cd_comment character varying, col_book_q1 numeric, col_book_q2 numeric, col_book_q3 numeric, col_book_q4 numeric, col_book_next_q1 numeric, col_book_next_q2 numeric, col_book_next_q3 numeric, col_book_next_q4 numeric, col_q1_adj_current numeric, col_q1_adj_ssp numeric, col_q1_adj_reserve numeric, col_q1_adj_comment character varying, col_q1_corrected_plan numeric, col_q1_pay_date date, col_q1_pay_amount numeric, col_q1_pay_comment character varying, col_q1_pay_act character varying, col_q1_booking numeric, col_q1_actual_m1 numeric, col_q1_actual_m2 numeric, col_q1_actual_m3 numeric, col_q1_actual_quarter numeric, col_q1_residual_after_booking numeric, col_q1_residual_after_actual numeric, col_q1_transfer_q2 numeric, col_q1_transfer_q2_delay_acts numeric, col_q1_transfer_q2_delay_procurement numeric, col_q1_transfer_q2_economy_rf numeric, col_q1_transfer_next_comment character varying, col_q1_transfer_q3 numeric, col_q1_transfer_q4 numeric, col_q1_transfer_far_comment character varying, col_q1_transfer_econ numeric, col_q1_total numeric, col_q2_target_change numeric, col_q2_base_correction numeric, col_q2_base_correction_comment character varying, col_q2_revision_inc numeric, col_q2_revision_seq numeric, col_q2_revision_comment character varying, col_q2_new_plan numeric, col_q2_adj_current numeric, col_q2_adj_ssp numeric, col_q2_adj_reserve numeric, col_q2_adj_comment character varying, col_q2_corrected_plan numeric, col_q2_pay_date date, col_q2_pay_amount numeric, col_q2_pay_comment character varying, col_q2_pay_act character varying, col_q2_booking numeric, col_q2_actual_m1 numeric, col_q2_actual_m2 numeric, col_q2_actual_m3 numeric, col_q2_actual_quarter numeric, col_q2_residual_after_booking numeric, col_q2_residual_after_actual numeric, col_q2_transfer_q3 numeric, col_q2_transfer_q3_delay_acts numeric, col_q2_transfer_q3_delay_procurement numeric, col_q2_transfer_q3_economy_rf numeric, col_q2_transfer_next_comment character varying, col_q2_transfer_q4 numeric, col_q2_transfer_far_comment character varying, col_q2_transfer_econ numeric, col_q2_total numeric, col_q3_target_change numeric, col_q3_base_correction numeric, col_q3_base_correction_comment character varying, col_q3_revision_inc numeric, col_q3_revision_seq numeric, col_q3_revision_comment character varying, col_q3_new_plan numeric, col_q3_adj_current numeric, col_q3_adj_ssp numeric, col_q3_adj_reserve numeric, col_q3_adj_comment character varying, col_q3_corrected_plan numeric, col_q3_pay_date date, col_q3_pay_amount numeric, col_q3_pay_comment character varying, col_q3_pay_act character varying, col_q3_booking numeric, col_q3_actual_m1 numeric, col_q3_actual_m2 numeric, col_q3_actual_m3 numeric, col_q3_actual_quarter numeric, col_q3_residual_after_booking numeric, col_q3_residual_after_actual numeric, col_q3_transfer_q4 numeric, col_q3_transfer_q4_delay_acts numeric, col_q3_transfer_q4_delay_procurement numeric, col_q3_transfer_q4_economy_rf numeric, col_q3_transfer_next_comment character varying, col_q3_transfer_econ numeric, col_q3_total numeric, col_q4_target_change numeric, col_q4_base_correction numeric, col_q4_base_correction_comment character varying, col_q4_revision_inc numeric, col_q4_revision_seq numeric, col_q4_revision_comment character varying, col_q4_new_plan numeric, col_q4_adj_current numeric, col_q4_adj_ssp numeric, col_q4_adj_reserve numeric, col_q4_adj_comment character varying, col_q4_corrected_plan numeric, col_q4_pay_date date, col_q4_pay_amount numeric, col_q4_pay_comment character varying, col_q4_pay_act character varying, col_q4_booking numeric, col_q4_actual_m1 numeric, col_q4_actual_m2 numeric, col_q4_actual_m3 numeric, col_q4_actual_spod numeric, col_q4_actual_quarter numeric, col_q4_residual_after_booking numeric, col_q4_residual_after_actual numeric, col_q4_transfer_econ numeric, col_q4_total numeric, col_fact_year numeric, col_pay_year numeric, col_economy_year numeric, col_internal_order character varying, col_cs_total numeric, col_cs_counterparty character varying, col_cs_deadline character varying, col_cs_comment character varying, col_cs_future_y1 numeric, col_cs_future_y2 numeric, col_cs_other_ssp numeric, col_al_contract_ref character varying, col_al_purpose character varying, col_al_order character varying, col_al_property character varying, col_ca_approved numeric, col_ca_protocol character varying, col_ca_note character varying, col_ckk_ceiling numeric, col_ckk_rf_sched character varying, col_ckk_deadline character varying, col_ckk_proc_plan character varying, col_ckk_proc_meth character varying, col_ckk_comment character varying, col_cd_addenda character varying, col_cd_exp_q1 numeric, col_cd_exp_q2 numeric, col_cd_exp_q3 numeric, col_cd_exp_q4 numeric, col_cd_rf_sched character varying, col_cd_exchange_rate numeric, col_cd_amount_foreign numeric, col_q1_adj_rf numeric, col_q1_pay_ho numeric, col_q1_pay_rf numeric, col_q2_adj_rf numeric, col_q2_pay_ho numeric, col_q2_pay_rf numeric, col_q3_adj_rf numeric, col_q3_pay_ho numeric, col_q3_pay_rf numeric, col_q4_adj_rf numeric, col_q4_pay_ho numeric, col_q4_pay_rf numeric, _sort_path integer[], _sort_rank bigint) + LANGUAGE plpgsql + STABLE +AS $function$ +#variable_conflict use_column +DECLARE + s_plan BOOL; s_seq_d BOOL; s_seq_s BOOL; s_appr BOOL; + s_cd BOOL; s_book BOOL; + s_cs BOOL; s_al BOOL; s_ca BOOL; s_ckk BOOL; -- НОВЫЕ блоки + s_q1 BOOL; s_q2 BOOL; s_q3 BOOL; s_q4 BOOL; s_tot BOOL; + s_need_ap BOOL; -- plan/seq/reserve нужны для approved + cp1..cp4 + v_focus_nodes INT[]; -- AURORA-1850: узлы фокуса (путь статей строк фокуса ∩ tw); NULL = весь лист + v_focus_eids INT[]; -- AURORA-1850: объединение desc_ids узлов фокуса +BEGIN + s_plan := p_sections IS NULL OR 'plan' = ANY(p_sections); + s_seq_d := p_sections IS NULL OR 'seq_dfip' = ANY(p_sections); + s_seq_s := p_sections IS NULL OR 'seq_ssp' = ANY(p_sections); + s_appr := p_sections IS NULL OR 'approved' = ANY(p_sections); + s_cd := p_sections IS NULL OR 'contract' = ANY(p_sections); + s_book := p_sections IS NULL OR 'booking' = ANY(p_sections); + s_cs := p_sections IS NULL OR 'contract_summary' = ANY(p_sections); + s_al := p_sections IS NULL OR 'allocation' = ANY(p_sections); + s_ca := p_sections IS NULL OR 'collegial' = ANY(p_sections); + s_ckk := p_sections IS NULL OR 'ckk' = ANY(p_sections); + s_q1 := p_sections IS NULL OR 'q1' = ANY(p_sections); + s_q2 := p_sections IS NULL OR 'q2' = ANY(p_sections); + s_q3 := p_sections IS NULL OR 'q3' = ANY(p_sections); + s_q4 := p_sections IS NULL OR 'q4' = ANY(p_sections); + s_tot := p_sections IS NULL OR 'totals' = ANY(p_sections); + s_need_ap := s_plan OR s_appr OR s_q1 OR s_q2 OR s_q3 OR s_q4; + + -- Фокус (D1): NULL = весь лист, '{}' = пустой результат. COALESCE обязателен: + -- пустой фокус не должен превратиться в NULL и провалиться в полную сборку. + IF p_focus_lines IS NOT NULL THEN + WITH fp AS ( + SELECT v3._expense_item_path_set( + (SELECT array_agg(DISTINCT bl.expense_item_id) + FROM v3.budget_line bl + WHERE bl.id = ANY(p_focus_lines))) AS ids + ), + fn AS ( + SELECT t.id, t.desc_ids + FROM v3.mv_expense_item_tree t + JOIN v3.expense_item_form_type eft ON eft.expense_item_id = t.id + JOIN v3.budget_form bf ON bf.id = p_form_id AND bf.form_type_code = eft.form_type_code + WHERE t.sheet = p_sheet + AND t.id = ANY(COALESCE((SELECT fp.ids FROM fp), '{}'::int[])) + ) + SELECT COALESCE((SELECT array_agg(fn.id) FROM fn), '{}'::int[]), + COALESCE((SELECT array_agg(DISTINCT d) FROM fn, unnest(fn.desc_ids) AS d), '{}'::int[]) + INTO v_focus_nodes, v_focus_eids; + END IF; + + RETURN QUERY + WITH + tw AS ( + SELECT t.id, t.section_code, t.item_id, t.num_group_id, t.name, + t.depth, t.path, t.parent_id, t.parent_item_id, t.desc_ids, + p.parent_item_id AS grandparent_item_id + FROM v3.mv_expense_item_tree t + LEFT JOIN v3.mv_expense_item_tree p ON p.id = t.parent_id + JOIN v3.expense_item_form_type eft ON eft.expense_item_id = t.id + JOIN v3.budget_form bf ON bf.id = p_form_id AND bf.form_type_code = eft.form_type_code + WHERE t.sheet = p_sheet + ), + page AS ( + SELECT bl.id AS lid, bl.expense_item_id AS eid, bl.name AS bname, bl.vsp_id, + bl.internal_order AS io + FROM v3.budget_line bl + JOIN v3.expense_item ei ON ei.id = bl.expense_item_id + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet + ), + tw_f AS ( + SELECT * FROM tw WHERE p_focus_lines IS NULL OR tw.id = ANY(v_focus_nodes) + ), + page_f AS ( + SELECT * FROM page WHERE p_focus_lines IS NULL OR page.lid = ANY(p_focus_lines) + ), + -- Скелет листа (D2): ключи всех узлов tw и всех строк page JOIN tw, без jsonb и сумм. + -- Ранг одинаков в обоих режимах; tie-break D6: узел раньше строки. + sk AS ( + SELECT n.k, + ROW_NUMBER() OVER (ORDER BY n._sort, n.lid NULLS FIRST) AS rnk + FROM ( + SELECT -t0.id::BIGINT AS k, NULL::INT AS lid, t0.path AS _sort FROM tw t0 + UNION ALL + SELECT pg0.lid::BIGINT, pg0.lid::INT, t1.path || ARRAY[pg0.lid::INT] + FROM page pg0 JOIN tw t1 ON t1.id = pg0.eid + ) n + ), + jp AS (SELECT p.* FROM v3.plan p JOIN page_f pg ON p.line_id = pg.lid WHERE s_need_ap), + jsd AS (SELECT s.* FROM v3.sequestration s JOIN page_f pg ON s.line_id = pg.lid WHERE s_need_ap AND s.actor='DFIP'), + jsg AS (SELECT s.* FROM v3.sequestration s JOIN page_f pg ON s.line_id = pg.lid WHERE s_need_ap AND s.actor='SSP_GO'), + jr AS (SELECT r.* FROM v3.reserve r JOIN page_f pg ON r.line_id = pg.lid WHERE s_need_ap), + jcd AS (SELECT c.* FROM v3.contract_detail c JOIN page_f pg ON c.line_id = pg.lid WHERE s_cd), + jck AS (SELECT c.* FROM v3.ckk c JOIN page_f pg ON c.line_id = pg.lid WHERE s_book OR s_ckk), + jcs AS (SELECT c.* FROM v3.contract_summary c JOIN page_f pg ON c.line_id = pg.lid WHERE s_cs), + jal AS (SELECT a.* FROM v3.allocation a JOIN page_f pg ON a.line_id = pg.lid WHERE s_al), + jca AS (SELECT c.* FROM v3.collegial_approval c JOIN page_f pg ON c.line_id = pg.lid WHERE s_ca), + jq1 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page_f pg ON q.line_id = pg.lid WHERE s_q1 AND q.quarter=1), + jq2 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page_f pg ON q.line_id = pg.lid WHERE s_q2 AND q.quarter=2), + jq3 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page_f pg ON q.line_id = pg.lid WHERE s_q3 AND q.quarter=3), + jq4 AS (SELECT q.* FROM v3.budget_line_quarter q JOIN page_f pg ON q.line_id = pg.lid WHERE (s_q4 OR s_tot) AND q.quarter=4), + + input_base AS ( + SELECT + pg.lid, pg.eid, pg.bname, pg.vsp_id, + v.address AS vsp_addr, + CASE + WHEN t.depth < 3 THEN t.item_id + WHEN t.depth = 3 THEN t.parent_item_id + WHEN t.depth = 4 THEN t.grandparent_item_id + END AS sc, + t.item_id AS ic, t.num_group_id AS ng, t.name AS ename, t.path AS tree_path, + COALESCE(p.plan_q1,0) AS pq1, COALESCE(p.plan_q2,0) AS pq2, + COALESCE(p.plan_q3,0) AS pq3, COALESCE(p.plan_q4,0) AS pq4, + p.comment AS pcmt, + COALESCE(sd.adj_q1,0) AS dq1, COALESCE(sd.adj_q2,0) AS dq2, + COALESCE(sd.adj_q3,0) AS dq3, COALESCE(sd.adj_q4,0) AS dq4, + sd.justification AS djust, + COALESCE(sg.adj_q1,0) AS gq1, COALESCE(sg.adj_q2,0) AS gq2, + COALESCE(sg.adj_q3,0) AS gq3, COALESCE(sg.adj_q4,0) AS gq4, + sg.justification AS gjust, + COALESCE(r.amount_q1,0) AS rq1, COALESCE(r.amount_q2,0) AS rq2, + COALESCE(r.amount_q3,0) AS rq3, COALESCE(r.amount_q4,0) AS rq4, + cd.counterparty AS cd_cp, cd.reference AS cd_ref, cd.contract_date AS cd_dt, + cd.subject AS cd_subj, cd.currency AS cd_cur, cd.ceiling_amount AS cd_ceil, + cd.vat_rate AS cd_vat, cd.deadline AS cd_dl, cd.payment_scheme AS cd_sch, + cd.act AS cd_act, cd.comment AS cd_cmt, + ck.expenses_q1 AS bk1, ck.expenses_q2 AS bk2, ck.expenses_q3 AS bk3, ck.expenses_q4 AS bk4, + ck.expenses_next_year_q1 AS bn1, ck.expenses_next_year_q2 AS bn2, + ck.expenses_next_year_q3 AS bn3, ck.expenses_next_year_q4 AS bn4, + pg.io AS io, + cs.total_amount AS cs_total, cs.counterparty AS cs_cp, cs.deadline AS cs_dl, + cs.comment AS cs_cmt, cs.future_payments_y1 AS cs_fy1, cs.future_payments_y2 AS cs_fy2, + cs.other_ssp_amount AS cs_oth, + al.contract_ref AS al_cref, al.allocation_purpose AS al_purp, + al.internal_order AS al_io, al.property_object AS al_prop, + ca.approved_amount AS ca_amt, ca.protocol_reference AS ca_proto, ca.note AS ca_note, + ck.ceiling_amount AS ckk_ceil, ck.rf_schedule AS ckk_rf, ck.delivery_deadline AS ckk_dl, + ck.procurement_plan AS ckk_pplan, ck.procurement_method AS ckk_pmeth, ck.comment AS ckk_cmt, + cd.addenda AS cd_add, cd.expenses_q1 AS cd_e1, cd.expenses_q2 AS cd_e2, + cd.expenses_q3 AS cd_e3, cd.expenses_q4 AS cd_e4, cd.rf_schedule AS cd_rf, + cd.exchange_rate AS cd_rate, cd.amount_foreign AS cd_fx, + q1.adj_rf AS q1_arf, q1.payment_amount_ho AS q1_pho, q1.payment_amount_rf AS q1_prf, + q2.adj_rf AS q2_arf, q2.payment_amount_ho AS q2_pho, q2.payment_amount_rf AS q2_prf, + q3.adj_rf AS q3_arf, q3.payment_amount_ho AS q3_pho, q3.payment_amount_rf AS q3_prf, + q4.adj_rf AS q4_arf, q4.payment_amount_ho AS q4_pho, q4.payment_amount_rf AS q4_prf, + q1.adj_current AS q1_ac, q1.adj_ssp AS q1_as, q1.adj_reserve AS q1_arv, q1.adj_comment AS q1_acmt, + q1.payment_date AS q1_pd, q1.payment_amount AS q1_pa, + q1.payment_comment AS q1_pcmt, + q1.payment_act AS q1_pact, + COALESCE( + q1.booking_amount, + CASE + WHEN p_sheet = 'AHR' THEN ck.expenses_q1 + WHEN ck.expenses_q1 > 0 THEN ck.expenses_q1 + ELSE cd.expenses_q1 + END + + ) AS q1_book, + q1.actual_m1 AS q1_m1, q1.actual_m2 AS q1_m2, q1.actual_m3 AS q1_m3, + q1.transfer_to_q2 AS q1_tq2, q1.transfer_to_q3 AS q1_tq3, q1.transfer_to_q4 AS q1_tq4, + q1.transfer_to_economy AS q1_te, + q1.transfer_delay_acts AS q1_tda, q1.transfer_delay_procurement AS q1_tdp, + q1.transfer_economy_rf AS q1_terf, + q1.transfer_next_comment AS q1_tnc, q1.transfer_far_comment AS q1_tfc, + q2.target_change AS q2_tc, q2.base_plan_correction AS q2_bc, q2.base_plan_correction_comment AS q2_bcc, + q2.plan_revision_increase AS q2_rinc, q2.plan_revision_sequester AS q2_rseq, + q2.plan_revision_comment AS q2_rcmt, + q2.adj_current AS q2_ac, q2.adj_ssp AS q2_as, q2.adj_reserve AS q2_arv, + q2.adj_comment AS q2_acmt, + q2.payment_date AS q2_pd, q2.payment_amount AS q2_pa, + q2.payment_comment AS q2_pcmt, q2.payment_act AS q2_pact, + COALESCE( + q2.booking_amount, + CASE + WHEN p_sheet = 'AHR' THEN ck.expenses_q2 + WHEN ck.expenses_q2 > 0 THEN ck.expenses_q2 + ELSE cd.expenses_q2 + END + + ) AS q2_book, + q2.actual_m1 AS q2_m1, q2.actual_m2 AS q2_m2, q2.actual_m3 AS q2_m3, + q2.transfer_to_q3 AS q2_tq3, q2.transfer_to_q4 AS q2_tq4, q2.transfer_to_economy AS q2_te, + q2.transfer_delay_acts AS q2_tda, q2.transfer_delay_procurement AS q2_tdp, + q2.transfer_economy_rf AS q2_terf, + q2.transfer_next_comment AS q2_tnc, q2.transfer_far_comment AS q2_tfc, + q3.target_change AS q3_tc, q3.base_plan_correction AS q3_bc, + q3.base_plan_correction_comment AS q3_bcc, + q3.plan_revision_increase AS q3_rinc, q3.plan_revision_sequester AS q3_rseq, + q3.plan_revision_comment AS q3_rcmt, + q3.adj_current AS q3_ac, q3.adj_ssp AS q3_as, q3.adj_reserve AS q3_arv, + q3.adj_comment AS q3_acmt, + q3.payment_date AS q3_pd, q3.payment_amount AS q3_pa, + q3.payment_comment AS q3_pcmt, q3.payment_act AS q3_pact, + COALESCE( + q3.booking_amount, + CASE + WHEN p_sheet = 'AHR' THEN ck.expenses_q3 + WHEN ck.expenses_q3 > 0 THEN ck.expenses_q3 + ELSE cd.expenses_q3 + END + + ) AS q3_book, + q3.actual_m1 AS q3_m1, q3.actual_m2 AS q3_m2, q3.actual_m3 AS q3_m3, + q3.transfer_to_q4 AS q3_tq4, q3.transfer_to_economy AS q3_te, + q3.transfer_delay_acts AS q3_tda, q3.transfer_delay_procurement AS q3_tdp, + q3.transfer_economy_rf AS q3_terf, q3.transfer_next_comment AS q3_tnc, + q4.target_change AS q4_tc, q4.base_plan_correction AS q4_bc, + q4.base_plan_correction_comment AS q4_bcc, + q4.plan_revision_increase AS q4_rinc, q4.plan_revision_sequester AS q4_rseq, + q4.plan_revision_comment AS q4_rcmt, + q4.adj_current AS q4_ac, q4.adj_ssp AS q4_as, q4.adj_reserve AS q4_arv, + q4.adj_comment AS q4_acmt, + q4.payment_date AS q4_pd, q4.payment_amount AS q4_pa, + q4.payment_comment AS q4_pcmt, q4.payment_act AS q4_pact, + COALESCE( + q4.booking_amount, + CASE + WHEN p_sheet = 'AHR' THEN ck.expenses_q4 + WHEN ck.expenses_q4 > 0 THEN ck.expenses_q4 + ELSE cd.expenses_q4 + END + + ) AS q4_book, + q4.actual_m1 AS q4_m1, q4.actual_m2 AS q4_m2, q4.actual_m3 AS q4_m3, q4.actual_spod AS q4_spod, + q4.transfer_to_economy AS q4_te, + COALESCE(p.plan_q1,0)+COALESCE(sd.adj_q1,0)+COALESCE(sg.adj_q1,0)+COALESCE(r.amount_q1,0) AS ap1, + COALESCE(p.plan_q2,0)+COALESCE(sd.adj_q2,0)+COALESCE(sg.adj_q2,0)+COALESCE(r.amount_q2,0) AS ap2, + COALESCE(p.plan_q3,0)+COALESCE(sd.adj_q3,0)+COALESCE(sg.adj_q3,0)+COALESCE(r.amount_q3,0) AS ap3, + COALESCE(p.plan_q4,0)+COALESCE(sd.adj_q4,0)+COALESCE(sg.adj_q4,0)+COALESCE(r.amount_q4,0) AS ap4 + FROM page_f pg + JOIN tw t ON t.id = pg.eid + LEFT JOIN v3.vsp v ON v.id = pg.vsp_id + LEFT JOIN jp p ON p.line_id = pg.lid + LEFT JOIN jsd sd ON sd.line_id = pg.lid + LEFT JOIN jsg sg ON sg.line_id = pg.lid + LEFT JOIN jr r ON r.line_id = pg.lid + LEFT JOIN jcd cd ON cd.line_id = pg.lid + LEFT JOIN jck ck ON ck.line_id = pg.lid + LEFT JOIN jcs cs ON cs.line_id = pg.lid + LEFT JOIN jal al ON al.line_id = pg.lid + LEFT JOIN jca ca ON ca.line_id = pg.lid + LEFT JOIN jq1 q1 ON q1.line_id = pg.lid + LEFT JOIN jq2 q2 ON q2.line_id = pg.lid + LEFT JOIN jq3 q3 ON q3.line_id = pg.lid + LEFT JOIN jq4 q4 ON q4.line_id = pg.lid + ), + enriched AS ( + SELECT b.*, + b.ap1 + COALESCE(b.q1_ac,0)+COALESCE(b.q1_as,0)+COALESCE(b.q1_arv,0) AS cp1, + COALESCE(b.q1_m1,0)+COALESCE(b.q1_m2,0)+COALESCE(b.q1_m3,0) AS aq1, + b.ap2 + COALESCE(b.q1_tq2,0) + COALESCE(b.q2_rinc,0)+COALESCE(b.q2_rseq,0) + COALESCE(b.q2_bc,0) AS np2, + COALESCE(b.q2_m1,0)+COALESCE(b.q2_m2,0)+COALESCE(b.q2_m3,0) AS aq2, + b.ap3 + COALESCE(b.q1_tq3,0)+COALESCE(b.q2_tq3,0) + COALESCE(b.q3_rinc,0)+COALESCE(b.q3_rseq,0) + COALESCE(b.q3_bc,0) AS np3, + COALESCE(b.q3_m1,0)+COALESCE(b.q3_m2,0)+COALESCE(b.q3_m3,0) AS aq3, + b.ap4 + COALESCE(b.q1_tq4,0)+COALESCE(b.q2_tq4,0)+COALESCE(b.q3_tq4,0) + COALESCE(b.q4_rinc,0)+COALESCE(b.q4_rseq,0) + COALESCE(b.q4_bc,0) AS np4, + COALESCE(b.q4_m1,0)+COALESCE(b.q4_m2,0)+COALESCE(b.q4_m3,0)+COALESCE(b.q4_spod,0) AS aq4 + FROM input_base b + ), + final_input AS ( + SELECT e.*, + e.np2 + COALESCE(e.q2_ac,0)+COALESCE(e.q2_as,0)+COALESCE(e.q2_arv,0) AS cp2, + e.np3 + COALESCE(e.q3_ac,0)+COALESCE(e.q3_as,0)+COALESCE(e.q3_arv,0) AS cp3, + e.np4 + COALESCE(e.q4_ac,0)+COALESCE(e.q4_as,0)+COALESCE(e.q4_arv,0) AS cp4 + FROM enriched e + ), + + agg AS ( + SELECT bl.expense_item_id AS eid, + SUM(p.plan_q1) AS sp1, SUM(p.plan_q2) AS sp2, SUM(p.plan_q3) AS sp3, SUM(p.plan_q4) AS sp4, + SUM(sd.adj_q1) AS sd1, SUM(sd.adj_q2) AS sd2, SUM(sd.adj_q3) AS sd3, SUM(sd.adj_q4) AS sd4, + SUM(sg.adj_q1) AS sg1, SUM(sg.adj_q2) AS sg2, SUM(sg.adj_q3) AS sg3, SUM(sg.adj_q4) AS sg4, + SUM(r.amount_q1) AS sr1, SUM(r.amount_q2) AS sr2, SUM(r.amount_q3) AS sr3, SUM(r.amount_q4) AS sr4, + SUM(ck.expenses_q1) AS bk1, SUM(ck.expenses_q2) AS bk2, SUM(ck.expenses_q3) AS bk3, SUM(ck.expenses_q4) AS bk4, + SUM(cd.expenses_q1) AS bkc1, SUM(cd.expenses_q2) AS bkc2, SUM(cd.expenses_q3) AS bkc3, SUM(cd.expenses_q4) AS bkc4, + SUM(ck.expenses_next_year_q1) AS bn1, SUM(ck.expenses_next_year_q2) AS bn2, + SUM(ck.expenses_next_year_q3) AS bn3, SUM(ck.expenses_next_year_q4) AS bn4 + FROM v3.budget_line bl + JOIN v3.expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN v3.plan p ON p.line_id = bl.id AND s_need_ap + LEFT JOIN v3.sequestration sd ON sd.line_id = bl.id AND sd.actor='DFIP' AND s_need_ap + LEFT JOIN v3.sequestration sg ON sg.line_id = bl.id AND sg.actor='SSP_GO' AND s_need_ap + LEFT JOIN v3.reserve r ON r.line_id = bl.id AND s_need_ap + LEFT JOIN v3.ckk ck ON ck.line_id = bl.id AND s_book + LEFT JOIN v3.contract_detail cd ON cd.line_id = bl.id AND s_cd + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet + AND (s_need_ap OR s_book OR s_cd) + AND (p_focus_lines IS NULL OR bl.expense_item_id = ANY(v_focus_eids)) + GROUP BY bl.expense_item_id + ), + aq_q AS ( + SELECT bl.expense_item_id AS eid, qrtrs.qrtr as quarter, + SUM(q.adj_current) AS ac, SUM(q.adj_ssp) AS as_v, SUM(q.adj_reserve) AS arv, + SUM(q.payment_amount) AS pa, + SUM( + COALESCE( + q.booking_amount, + CASE + WHEN qrtrs.qrtr = 1 + THEN CASE + WHEN p_sheet = 'AHR' THEN ck.expenses_q1 + WHEN ck.expenses_q1 > 0 THEN ck.expenses_q1 + ELSE cd.expenses_q1 + END + WHEN qrtrs.qrtr = 2 THEN CASE + WHEN p_sheet = 'AHR' THEN ck.expenses_q2 + WHEN ck.expenses_q2 > 0 THEN ck.expenses_q2 + ELSE cd.expenses_q2 + END + WHEN qrtrs.qrtr = 3 THEN CASE + WHEN p_sheet = 'AHR' THEN ck.expenses_q3 + WHEN ck.expenses_q3 > 0 THEN ck.expenses_q3 + ELSE cd.expenses_q3 + END + WHEN qrtrs.qrtr = 4 THEN CASE + WHEN p_sheet = 'AHR' THEN ck.expenses_q4 + WHEN ck.expenses_q4 > 0 THEN ck.expenses_q4 + ELSE cd.expenses_q4 + END + END + + )) AS bk, + SUM(q.actual_m1) AS m1, SUM(q.actual_m2) AS m2, SUM(q.actual_m3) AS m3, + SUM(q.actual_spod) AS spod, + SUM(q.transfer_to_q2) AS tq2, SUM(q.transfer_to_q3) AS tq3, SUM(q.transfer_to_q4) AS tq4, + SUM(q.transfer_to_economy) AS te, + SUM(q.transfer_delay_acts) AS tda, + SUM(q.transfer_delay_procurement) AS tdp, + SUM(q.transfer_economy_rf) AS terf, + SUM(COALESCE(q.plan_revision_increase,0) + + COALESCE(q.plan_revision_sequester,0) + + COALESCE(q.target_change,0) + + COALESCE(q.base_plan_correction,0)) AS rev + FROM v3.budget_line bl + CROSS JOIN (VALUES (1), (2), (3), (4)) AS qrtrs(qrtr) + + JOIN v3.expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN v3.budget_line_quarter q ON q.line_id = bl.id AND q.quarter = qrtrs.qrtr + LEFT JOIN v3.ckk ck ON bl.id = ck.line_id + LEFT JOIN v3.contract_detail cd ON bl.id = cd.line_id + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet + AND (p_focus_lines IS NULL OR bl.expense_item_id = ANY(v_focus_eids)) + GROUP BY bl.expense_item_id, qrtrs.qrtr + ), + tw_agg AS ( + SELECT tw.id, + SUM(a.sp1) AS sp1, SUM(a.sp2) AS sp2, SUM(a.sp3) AS sp3, SUM(a.sp4) AS sp4, + SUM(a.sd1) AS sd1, SUM(a.sd2) AS sd2, SUM(a.sd3) AS sd3, SUM(a.sd4) AS sd4, + SUM(a.sg1) AS sg1, SUM(a.sg2) AS sg2, SUM(a.sg3) AS sg3, SUM(a.sg4) AS sg4, + SUM(a.sr1) AS sr1, SUM(a.sr2) AS sr2, SUM(a.sr3) AS sr3, SUM(a.sr4) AS sr4, + SUM(a.bk1) AS bk1, SUM(a.bk2) AS bk2, SUM(a.bk3) AS bk3, SUM(a.bk4) AS bk4, + SUM(a.bkc1) AS bkc1, SUM(a.bkc2) AS bkc2, SUM(a.bkc3) AS bkc3, SUM(a.bkc4) AS bkc4, + SUM(a.bn1) AS bn1, SUM(a.bn2) AS bn2, SUM(a.bn3) AS bn3, SUM(a.bn4) AS bn4 + FROM tw_f tw LEFT JOIN agg a ON a.eid = ANY(tw.desc_ids) + GROUP BY tw.id + ), + tw_aq1 AS ( + SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.bk) AS bk, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, + SUM(b.tq2) AS tq2, SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4, SUM(b.te) AS te, + SUM(b.tda) AS tda, SUM(b.tdp) AS tdp, SUM(b.terf) AS terf + FROM tw_f tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=1 GROUP BY tw.id + ), + tw_aq2 AS ( + SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.bk) AS bk, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, + SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4, SUM(b.te) AS te, SUM(b.rev) AS rev, + SUM(b.tda) AS tda, SUM(b.tdp) AS tdp, SUM(b.terf) AS terf + FROM tw_f tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=2 GROUP BY tw.id + ), + tw_aq3 AS ( + SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.bk) AS bk, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, + SUM(b.tq4) AS tq4, SUM(b.te) AS te, SUM(b.rev) AS rev, + SUM(b.tda) AS tda, SUM(b.tdp) AS tdp, SUM(b.terf) AS terf + FROM tw_f tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=3 GROUP BY tw.id + ), + tw_aq4 AS ( + SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as_v) AS as_v, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.bk) AS bk, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, SUM(b.spod) AS spod, + SUM(b.te) AS te, SUM(b.rev) AS rev + FROM tw_f tw LEFT JOIN aq_q b ON b.eid = ANY(tw.desc_ids) AND b.quarter=4 GROUP BY tw.id + ) + + SELECT sub.row_type, sub.depth, sub.line_id, sub.col_section_code, sub.col_item_id, sub.col_num_group_id, sub.col_name, sub.col_vsp_id, sub.col_vsp_address, sub.col_plan_q1, sub.col_plan_q2, sub.col_plan_q3, sub.col_plan_q4, sub.col_plan_year, sub.col_plan_comment, sub.col_seq_dfip_q1, sub.col_seq_dfip_q2, sub.col_seq_dfip_q3, sub.col_seq_dfip_q4, sub.col_seq_dfip_year, sub.col_seq_dfip_just, sub.col_seq_ssp_q1, sub.col_seq_ssp_q2, sub.col_seq_ssp_q3, sub.col_seq_ssp_q4, sub.col_seq_ssp_year, sub.col_seq_ssp_just, sub.col_appr_q1, sub.col_appr_q2, sub.col_appr_q3, sub.col_appr_q4, sub.col_appr_year, sub.col_cd_counterparty, sub.col_cd_reference, sub.col_cd_date, sub.col_cd_subject, sub.col_cd_currency, sub.col_cd_ceiling, sub.col_cd_vat_rate, sub.col_cd_deadline, sub.col_cd_scheme, sub.col_cd_act, sub.col_cd_comment, sub.col_book_q1, sub.col_book_q2, sub.col_book_q3, sub.col_book_q4, sub.col_book_next_q1, sub.col_book_next_q2, sub.col_book_next_q3, sub.col_book_next_q4, sub.col_q1_adj_current, sub.col_q1_adj_ssp, sub.col_q1_adj_reserve, sub.col_q1_adj_comment, sub.col_q1_corrected_plan, sub.col_q1_pay_date, sub.col_q1_pay_amount, sub.col_q1_pay_comment, sub.col_q1_pay_act, sub.col_q1_booking, sub.col_q1_actual_m1, sub.col_q1_actual_m2, sub.col_q1_actual_m3, sub.col_q1_actual_quarter, sub.col_q1_residual_after_booking, sub.col_q1_residual_after_actual, sub.col_q1_transfer_q2, sub.col_q1_transfer_q2_delay_acts, sub.col_q1_transfer_q2_delay_procurement, sub.col_q1_transfer_q2_economy_rf, sub.col_q1_transfer_next_comment, sub.col_q1_transfer_q3, sub.col_q1_transfer_q4, sub.col_q1_transfer_far_comment, sub.col_q1_transfer_econ, sub.col_q1_total, sub.col_q2_target_change, sub.col_q2_base_correction, sub.col_q2_base_correction_comment, sub.col_q2_revision_inc, sub.col_q2_revision_seq, sub.col_q2_revision_comment, sub.col_q2_new_plan, sub.col_q2_adj_current, sub.col_q2_adj_ssp, sub.col_q2_adj_reserve, sub.col_q2_adj_comment, sub.col_q2_corrected_plan, sub.col_q2_pay_date, sub.col_q2_pay_amount, sub.col_q2_pay_comment, sub.col_q2_pay_act, sub.col_q2_booking, sub.col_q2_actual_m1, sub.col_q2_actual_m2, sub.col_q2_actual_m3, sub.col_q2_actual_quarter, sub.col_q2_residual_after_booking, sub.col_q2_residual_after_actual, sub.col_q2_transfer_q3, sub.col_q2_transfer_q3_delay_acts, sub.col_q2_transfer_q3_delay_procurement, sub.col_q2_transfer_q3_economy_rf, sub.col_q2_transfer_next_comment, sub.col_q2_transfer_q4, sub.col_q2_transfer_far_comment, sub.col_q2_transfer_econ, sub.col_q2_total, sub.col_q3_target_change, sub.col_q3_base_correction, sub.col_q3_base_correction_comment, sub.col_q3_revision_inc, sub.col_q3_revision_seq, sub.col_q3_revision_comment, sub.col_q3_new_plan, sub.col_q3_adj_current, sub.col_q3_adj_ssp, sub.col_q3_adj_reserve, sub.col_q3_adj_comment, sub.col_q3_corrected_plan, sub.col_q3_pay_date, sub.col_q3_pay_amount, sub.col_q3_pay_comment, sub.col_q3_pay_act, sub.col_q3_booking, sub.col_q3_actual_m1, sub.col_q3_actual_m2, sub.col_q3_actual_m3, sub.col_q3_actual_quarter, sub.col_q3_residual_after_booking, sub.col_q3_residual_after_actual, sub.col_q3_transfer_q4, sub.col_q3_transfer_q4_delay_acts, sub.col_q3_transfer_q4_delay_procurement, sub.col_q3_transfer_q4_economy_rf, sub.col_q3_transfer_next_comment, sub.col_q3_transfer_econ, sub.col_q3_total, sub.col_q4_target_change, sub.col_q4_base_correction, sub.col_q4_base_correction_comment, sub.col_q4_revision_inc, sub.col_q4_revision_seq, sub.col_q4_revision_comment, sub.col_q4_new_plan, sub.col_q4_adj_current, sub.col_q4_adj_ssp, sub.col_q4_adj_reserve, sub.col_q4_adj_comment, sub.col_q4_corrected_plan, sub.col_q4_pay_date, sub.col_q4_pay_amount, sub.col_q4_pay_comment, sub.col_q4_pay_act, sub.col_q4_booking, sub.col_q4_actual_m1, sub.col_q4_actual_m2, sub.col_q4_actual_m3, sub.col_q4_actual_spod, sub.col_q4_actual_quarter, sub.col_q4_residual_after_booking, sub.col_q4_residual_after_actual, sub.col_q4_transfer_econ, sub.col_q4_total, sub.col_fact_year, sub.col_pay_year, sub.col_economy_year, sub.col_internal_order, sub.col_cs_total, sub.col_cs_counterparty, sub.col_cs_deadline, sub.col_cs_comment, sub.col_cs_future_y1, sub.col_cs_future_y2, sub.col_cs_other_ssp, sub.col_al_contract_ref, sub.col_al_purpose, sub.col_al_order, sub.col_al_property, sub.col_ca_approved, sub.col_ca_protocol, sub.col_ca_note, sub.col_ckk_ceiling, sub.col_ckk_rf_sched, sub.col_ckk_deadline, sub.col_ckk_proc_plan, sub.col_ckk_proc_meth, sub.col_ckk_comment, sub.col_cd_addenda, sub.col_cd_exp_q1, sub.col_cd_exp_q2, sub.col_cd_exp_q3, sub.col_cd_exp_q4, sub.col_cd_rf_sched, sub.col_cd_exchange_rate, sub.col_cd_amount_foreign, sub.col_q1_adj_rf, sub.col_q1_pay_ho, sub.col_q1_pay_rf, sub.col_q2_adj_rf, sub.col_q2_pay_ho, sub.col_q2_pay_rf, sub.col_q3_adj_rf, sub.col_q3_pay_ho, sub.col_q3_pay_rf, sub.col_q4_adj_rf, sub.col_q4_pay_ho, sub.col_q4_pay_rf, sub._sort_path, sk.rnk + FROM ( + SELECT + 'INPUT'::VARCHAR, array_length(f.tree_path, 1)::INT, f.lid::INT, + f.sc, f.ic, f.ng, COALESCE(f.bname, f.ename), f.vsp_id, f.vsp_addr, + CASE WHEN s_plan THEN f.pq1 END, CASE WHEN s_plan THEN f.pq2 END, + CASE WHEN s_plan THEN f.pq3 END, CASE WHEN s_plan THEN f.pq4 END, + CASE WHEN s_plan THEN f.pq1+f.pq2+f.pq3+f.pq4 END, CASE WHEN s_plan THEN f.pcmt END, + CASE WHEN s_seq_d THEN f.dq1 END, CASE WHEN s_seq_d THEN f.dq2 END, + CASE WHEN s_seq_d THEN f.dq3 END, CASE WHEN s_seq_d THEN f.dq4 END, + CASE WHEN s_seq_d THEN f.dq1+f.dq2+f.dq3+f.dq4 END, CASE WHEN s_seq_d THEN f.djust END, + CASE WHEN s_seq_s THEN f.gq1 END, CASE WHEN s_seq_s THEN f.gq2 END, + CASE WHEN s_seq_s THEN f.gq3 END, CASE WHEN s_seq_s THEN f.gq4 END, + CASE WHEN s_seq_s THEN f.gq1+f.gq2+f.gq3+f.gq4 END, CASE WHEN s_seq_s THEN f.gjust END, + CASE WHEN s_appr THEN f.ap1 END, CASE WHEN s_appr THEN f.ap2 END, + CASE WHEN s_appr THEN f.ap3 END, CASE WHEN s_appr THEN f.ap4 END, + CASE WHEN s_appr THEN f.ap1+f.ap2+f.ap3+f.ap4 END, + CASE WHEN s_cd THEN f.cd_cp END, CASE WHEN s_cd THEN f.cd_ref END, + CASE WHEN s_cd THEN f.cd_dt END, CASE WHEN s_cd THEN f.cd_subj END, + CASE WHEN s_cd THEN f.cd_cur END, CASE WHEN s_cd THEN f.cd_ceil END, + CASE WHEN s_cd THEN f.cd_vat END, CASE WHEN s_cd THEN f.cd_dl END, + CASE WHEN s_cd THEN f.cd_sch END, CASE WHEN s_cd THEN f.cd_act END, + CASE WHEN s_cd THEN f.cd_cmt END, + CASE WHEN s_book THEN f.bk1 END, CASE WHEN s_book THEN f.bk2 END, + CASE WHEN s_book THEN f.bk3 END, CASE WHEN s_book THEN f.bk4 END, + CASE WHEN s_book THEN f.bn1 END, CASE WHEN s_book THEN f.bn2 END, + CASE WHEN s_book THEN f.bn3 END, CASE WHEN s_book THEN f.bn4 END, + CASE WHEN s_q1 THEN f.q1_ac END, CASE WHEN s_q1 THEN f.q1_as END, CASE WHEN s_q1 THEN f.q1_arv END, + CASE WHEN s_q1 THEN f.q1_acmt END, CASE WHEN s_q1 THEN f.cp1 END, + CASE WHEN s_q1 THEN f.q1_pd END, CASE WHEN s_q1 THEN f.q1_pa END, + CASE WHEN s_q1 THEN f.q1_pcmt END, + CASE WHEN s_q1 THEN f.q1_pact END, CASE WHEN s_q1 THEN f.q1_book END, + CASE WHEN s_q1 THEN f.q1_m1 END, CASE WHEN s_q1 THEN f.q1_m2 END, CASE WHEN s_q1 THEN f.q1_m3 END, + CASE WHEN s_q1 THEN f.aq1 END, + CASE WHEN s_q1 THEN f.cp1 - COALESCE(f.q1_book,0) END, -- residual_after_booking + CASE WHEN s_q1 THEN f.cp1 - f.aq1 END, -- residual_after_actual + CASE WHEN s_q1 THEN f.q1_tq2 END, + CASE WHEN s_q1 THEN f.q1_tda END, CASE WHEN s_q1 THEN f.q1_tdp END, CASE WHEN s_q1 THEN f.q1_terf END, + CASE WHEN s_q1 THEN f.q1_tnc END, + CASE WHEN s_q1 THEN f.q1_tq3 END, CASE WHEN s_q1 THEN f.q1_tq4 END, + CASE WHEN s_q1 THEN f.q1_tfc END, + CASE WHEN s_q1 THEN f.q1_te END, + CASE WHEN s_q1 THEN + COALESCE(f.q1_tda,0)+COALESCE(f.q1_tdp,0)+COALESCE(f.q1_terf,0) + +COALESCE(f.q1_tq3,0)+COALESCE(f.q1_tq4,0)+COALESCE(f.q1_te,0) + END, + CASE WHEN s_q2 THEN f.q2_tc END, CASE WHEN s_q2 THEN f.q2_bc END, CASE WHEN s_q2 THEN f.q2_bcc END, + CASE WHEN s_q2 THEN f.q2_rinc END, CASE WHEN s_q2 THEN f.q2_rseq END, + CASE WHEN s_q2 THEN f.q2_rcmt END, + CASE WHEN s_q2 THEN f.np2 END, + CASE WHEN s_q2 THEN f.q2_ac END, CASE WHEN s_q2 THEN f.q2_as END, CASE WHEN s_q2 THEN f.q2_arv END, + CASE WHEN s_q2 THEN f.q2_acmt END, + CASE WHEN s_q2 THEN f.cp2 END, + CASE WHEN s_q2 THEN f.q2_pd END, CASE WHEN s_q2 THEN f.q2_pa END, + CASE WHEN s_q2 THEN f.q2_pcmt END, CASE WHEN s_q2 THEN f.q2_pact END, + CASE WHEN s_q2 THEN f.q2_book END, + CASE WHEN s_q2 THEN f.q2_m1 END, CASE WHEN s_q2 THEN f.q2_m2 END, CASE WHEN s_q2 THEN f.q2_m3 END, + CASE WHEN s_q2 THEN f.aq2 END, + CASE WHEN s_q2 THEN f.cp2 - COALESCE(f.q2_book,0) END, + CASE WHEN s_q2 THEN f.cp2 - f.aq2 END, + CASE WHEN s_q2 THEN f.q2_tq3 END, + CASE WHEN s_q2 THEN f.q2_tda END, CASE WHEN s_q2 THEN f.q2_tdp END, CASE WHEN s_q2 THEN f.q2_terf END, + CASE WHEN s_q2 THEN f.q2_tnc END, + CASE WHEN s_q2 THEN f.q2_tq4 END, + CASE WHEN s_q2 THEN f.q2_tfc END, + CASE WHEN s_q2 THEN f.q2_te END, + CASE WHEN s_q2 THEN + COALESCE(f.q2_tda,0)+COALESCE(f.q2_tdp,0)+COALESCE(f.q2_terf,0) + +COALESCE(f.q2_tq4,0)+COALESCE(f.q2_te,0) + END, + CASE WHEN s_q3 THEN f.q3_tc END, CASE WHEN s_q3 THEN f.q3_bc END, + CASE WHEN s_q3 THEN f.q3_bcc END, + CASE WHEN s_q3 THEN f.q3_rinc END, CASE WHEN s_q3 THEN f.q3_rseq END, + CASE WHEN s_q3 THEN f.q3_rcmt END, + CASE WHEN s_q3 THEN f.np3 END, + CASE WHEN s_q3 THEN f.q3_ac END, CASE WHEN s_q3 THEN f.q3_as END, CASE WHEN s_q3 THEN f.q3_arv END, + CASE WHEN s_q3 THEN f.q3_acmt END, + CASE WHEN s_q3 THEN f.cp3 END, + CASE WHEN s_q3 THEN f.q3_pd END, CASE WHEN s_q3 THEN f.q3_pa END, + CASE WHEN s_q3 THEN f.q3_pcmt END, CASE WHEN s_q3 THEN f.q3_pact END, + CASE WHEN s_q3 THEN f.q3_book END, + CASE WHEN s_q3 THEN f.q3_m1 END, CASE WHEN s_q3 THEN f.q3_m2 END, CASE WHEN s_q3 THEN f.q3_m3 END, + CASE WHEN s_q3 THEN f.aq3 END, + CASE WHEN s_q3 THEN f.cp3 - COALESCE(f.q3_book,0) END, + CASE WHEN s_q3 THEN f.cp3 - f.aq3 END, + CASE WHEN s_q3 THEN f.q3_tq4 END, + CASE WHEN s_q3 THEN f.q3_tda END, CASE WHEN s_q3 THEN f.q3_tdp END, CASE WHEN s_q3 THEN f.q3_terf END, + CASE WHEN s_q3 THEN f.q3_tnc END, + CASE WHEN s_q3 THEN f.q3_te END, + CASE WHEN s_q3 THEN + COALESCE(f.q3_tda,0)+COALESCE(f.q3_tdp,0)+COALESCE(f.q3_terf,0) + +COALESCE(f.q3_te,0) + END, + CASE WHEN s_q4 THEN f.q4_tc END, CASE WHEN s_q4 THEN f.q4_bc END, + CASE WHEN s_q4 THEN f.q4_bcc END, + CASE WHEN s_q4 THEN f.q4_rinc END, CASE WHEN s_q4 THEN f.q4_rseq END, + CASE WHEN s_q4 THEN f.q4_rcmt END, + CASE WHEN s_q4 THEN f.np4 END, + CASE WHEN s_q4 THEN f.q4_ac END, CASE WHEN s_q4 THEN f.q4_as END, CASE WHEN s_q4 THEN f.q4_arv END, + CASE WHEN s_q4 THEN f.q4_acmt END, + CASE WHEN s_q4 THEN f.cp4 END, + CASE WHEN s_q4 THEN f.q4_pd END, CASE WHEN s_q4 THEN f.q4_pa END, + CASE WHEN s_q4 THEN f.q4_pcmt END, CASE WHEN s_q4 THEN f.q4_pact END, + CASE WHEN s_q4 THEN f.q4_book END, + CASE WHEN s_q4 THEN f.q4_m1 END, CASE WHEN s_q4 THEN f.q4_m2 END, CASE WHEN s_q4 THEN f.q4_m3 END, + CASE WHEN s_q4 THEN f.q4_spod END, + CASE WHEN s_q4 THEN f.aq4 END, + CASE WHEN s_q4 THEN f.cp4 - COALESCE(f.q4_book,0) END, + CASE WHEN s_q4 THEN f.cp4 - f.aq4 END, + CASE WHEN s_q4 THEN f.q4_te END, + CASE WHEN s_q4 THEN f.q4_te END, + CASE WHEN s_tot THEN f.aq1+f.aq2+f.aq3+f.aq4 END, + CASE WHEN s_tot THEN COALESCE(f.q1_pa,0)+COALESCE(f.q2_pa,0)+COALESCE(f.q3_pa,0)+COALESCE(f.q4_pa,0) END, + CASE WHEN s_tot THEN f.cp4 - f.aq4 END, + + f.io, + CASE WHEN s_cs THEN f.cs_total END, CASE WHEN s_cs THEN f.cs_cp END, + CASE WHEN s_cs THEN f.cs_dl END, CASE WHEN s_cs THEN f.cs_cmt END, + CASE WHEN s_cs THEN f.cs_fy1 END, CASE WHEN s_cs THEN f.cs_fy2 END, + CASE WHEN s_cs THEN f.cs_oth END, + CASE WHEN s_al THEN f.al_cref END, CASE WHEN s_al THEN f.al_purp END, + CASE WHEN s_al THEN f.al_io END, CASE WHEN s_al THEN f.al_prop END, + CASE WHEN s_ca THEN f.ca_amt END, CASE WHEN s_ca THEN f.ca_proto END, + CASE WHEN s_ca THEN f.ca_note END, + CASE WHEN s_ckk THEN f.ckk_ceil END, CASE WHEN s_ckk THEN f.ckk_rf END, + CASE WHEN s_ckk THEN f.ckk_dl END, CASE WHEN s_ckk THEN f.ckk_pplan END, + CASE WHEN s_ckk THEN f.ckk_pmeth END, CASE WHEN s_ckk THEN f.ckk_cmt END, + CASE WHEN s_cd THEN f.cd_add END, CASE WHEN s_cd THEN f.cd_e1 END, + CASE WHEN s_cd THEN f.cd_e2 END, CASE WHEN s_cd THEN f.cd_e3 END, + CASE WHEN s_cd THEN f.cd_e4 END, CASE WHEN s_cd THEN f.cd_rf END, + CASE WHEN s_cd THEN f.cd_rate END, CASE WHEN s_cd THEN f.cd_fx END, + CASE WHEN s_q1 THEN f.q1_arf END, CASE WHEN s_q1 THEN f.q1_pho END, CASE WHEN s_q1 THEN f.q1_prf END, + CASE WHEN s_q2 THEN f.q2_arf END, CASE WHEN s_q2 THEN f.q2_pho END, CASE WHEN s_q2 THEN f.q2_prf END, + CASE WHEN s_q3 THEN f.q3_arf END, CASE WHEN s_q3 THEN f.q3_pho END, CASE WHEN s_q3 THEN f.q3_prf END, + CASE WHEN s_q4 THEN f.q4_arf END, CASE WHEN s_q4 THEN f.q4_pho END, CASE WHEN s_q4 THEN f.q4_prf END, + + f.tree_path || ARRAY[f.lid::INT] AS _sort, + f.lid::BIGINT AS _k + FROM final_input f + + UNION ALL + + SELECT + CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' WHEN 4 THEN 'SUB_SUB_ITEM' END::VARCHAR, + t.depth, NULL::INT, + CASE + WHEN t.depth < 3 THEN t.item_id + WHEN t.depth = 3 THEN t.parent_item_id + WHEN t.depth = 4 THEN t.grandparent_item_id + END, + CASE WHEN t.depth >= 3 THEN t.item_id ELSE NULL END, + t.num_group_id, t.name, NULL::INT, NULL::VARCHAR, + CASE WHEN s_plan THEN ta.sp1 END, CASE WHEN s_plan THEN ta.sp2 END, + CASE WHEN s_plan THEN ta.sp3 END, CASE WHEN s_plan THEN ta.sp4 END, + CASE WHEN s_plan THEN COALESCE(ta.sp1,0)+COALESCE(ta.sp2,0)+COALESCE(ta.sp3,0)+COALESCE(ta.sp4,0) END, + NULL::VARCHAR, + CASE WHEN s_seq_d THEN ta.sd1 END, CASE WHEN s_seq_d THEN ta.sd2 END, + CASE WHEN s_seq_d THEN ta.sd3 END, CASE WHEN s_seq_d THEN ta.sd4 END, + CASE WHEN s_seq_d THEN COALESCE(ta.sd1,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sd4,0) END, + NULL::VARCHAR, + CASE WHEN s_seq_s THEN ta.sg1 END, CASE WHEN s_seq_s THEN ta.sg2 END, + CASE WHEN s_seq_s THEN ta.sg3 END, CASE WHEN s_seq_s THEN ta.sg4 END, + CASE WHEN s_seq_s THEN COALESCE(ta.sg1,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sg4,0) END, + NULL::VARCHAR, + CASE WHEN s_appr THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0) END, + CASE WHEN s_appr THEN COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0) END, + CASE WHEN s_appr THEN COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0) END, + CASE WHEN s_appr THEN COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0) END, + CASE WHEN s_appr THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0) + +COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0) + +COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0) + +COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0) END, + NULL::VARCHAR, NULL::VARCHAR, NULL::DATE, NULL::VARCHAR, + NULL::VARCHAR, NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, + NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_book THEN ta.bk1 END, CASE WHEN s_book THEN ta.bk2 END, + CASE WHEN s_book THEN ta.bk3 END, CASE WHEN s_book THEN ta.bk4 END, + CASE WHEN s_book THEN ta.bn1 END, CASE WHEN s_book THEN ta.bn2 END, + CASE WHEN s_book THEN ta.bn3 END, CASE WHEN s_book THEN ta.bn4 END, + CASE WHEN s_q1 THEN tb1.ac END, CASE WHEN s_q1 THEN tb1.as_v END, CASE WHEN s_q1 THEN tb1.arv END, + NULL::VARCHAR, + CASE WHEN s_q1 THEN COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0)+COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arv,0) END, + NULL::DATE, + CASE WHEN s_q1 THEN tb1.pa END, + NULL::VARCHAR, -- pay_comment + NULL::VARCHAR, -- pay_act + CASE WHEN s_q1 THEN tb1.bk END, -- booking (rollup) + CASE WHEN s_q1 THEN tb1.m1 END, CASE WHEN s_q1 THEN tb1.m2 END, CASE WHEN s_q1 THEN tb1.m3 END, + CASE WHEN s_q1 THEN COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0) END, + CASE WHEN s_q1 THEN + (COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0)+COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arv,0)) + - COALESCE(tb1.bk,0) + END, + CASE WHEN s_q1 THEN + (COALESCE(ta.sp1,0)+COALESCE(ta.sd1,0)+COALESCE(ta.sg1,0)+COALESCE(ta.sr1,0)+COALESCE(tb1.ac,0)+COALESCE(tb1.as_v,0)+COALESCE(tb1.arv,0)) + - (COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)) + END, + CASE WHEN s_q1 THEN tb1.tq2 END, + CASE WHEN s_q1 THEN tb1.tda END, CASE WHEN s_q1 THEN tb1.tdp END, CASE WHEN s_q1 THEN tb1.terf END, + NULL::VARCHAR, -- transfer_next_comment (text, no rollup) + CASE WHEN s_q1 THEN tb1.tq3 END, CASE WHEN s_q1 THEN tb1.tq4 END, + NULL::VARCHAR, -- far comment + CASE WHEN s_q1 THEN tb1.te END, + CASE WHEN s_q1 THEN + COALESCE(tb1.tda,0)+COALESCE(tb1.tdp,0)+COALESCE(tb1.terf,0) + +COALESCE(tb1.tq3,0)+COALESCE(tb1.tq4,0)+COALESCE(tb1.te,0) + END, + NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- target_change/base_correction + NULL::NUMERIC, NULL::NUMERIC, -- revision_inc/revision_seq + NULL::VARCHAR, -- revision_comment + CASE WHEN s_q2 THEN + COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0) + END, + CASE WHEN s_q2 THEN tb2.ac END, CASE WHEN s_q2 THEN tb2.as_v END, CASE WHEN s_q2 THEN tb2.arv END, + NULL::VARCHAR, -- adj_comment + CASE WHEN s_q2 THEN + COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arv,0) + END, + NULL::DATE, + CASE WHEN s_q2 THEN tb2.pa END, + NULL::VARCHAR, -- pay_comment + NULL::VARCHAR, -- pay_act + CASE WHEN s_q2 THEN tb2.bk END, -- booking (rollup) + CASE WHEN s_q2 THEN tb2.m1 END, CASE WHEN s_q2 THEN tb2.m2 END, CASE WHEN s_q2 THEN tb2.m3 END, + CASE WHEN s_q2 THEN COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0) END, + CASE WHEN s_q2 THEN + (COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arv,0)) + - COALESCE(tb2.bk,0) + END, + CASE WHEN s_q2 THEN + (COALESCE(ta.sp2,0)+COALESCE(ta.sd2,0)+COALESCE(ta.sg2,0)+COALESCE(ta.sr2,0)+COALESCE(tb1.tq2,0)+COALESCE(tb2.rev,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as_v,0)+COALESCE(tb2.arv,0)) + - (COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)) + END, + CASE WHEN s_q2 THEN tb2.tq3 END, + CASE WHEN s_q2 THEN tb2.tda END, CASE WHEN s_q2 THEN tb2.tdp END, CASE WHEN s_q2 THEN tb2.terf END, + NULL::VARCHAR, -- transfer_next_comment (text, no rollup) + CASE WHEN s_q2 THEN tb2.tq4 END, + NULL::VARCHAR, -- far comment + CASE WHEN s_q2 THEN tb2.te END, + CASE WHEN s_q2 THEN + COALESCE(tb2.tda,0)+COALESCE(tb2.tdp,0)+COALESCE(tb2.terf,0) + +COALESCE(tb2.tq4,0)+COALESCE(tb2.te,0) + END, + NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- target_change/base_correction/comment + NULL::NUMERIC, NULL::NUMERIC, -- revision_inc/revision_seq + NULL::VARCHAR, + CASE WHEN s_q3 THEN + COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0) + END, + CASE WHEN s_q3 THEN tb3.ac END, CASE WHEN s_q3 THEN tb3.as_v END, CASE WHEN s_q3 THEN tb3.arv END, + NULL::VARCHAR, + CASE WHEN s_q3 THEN + COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arv,0) + END, + NULL::DATE, + CASE WHEN s_q3 THEN tb3.pa END, + NULL::VARCHAR, -- pay_comment + NULL::VARCHAR, -- pay_act + CASE WHEN s_q3 THEN tb3.bk END, -- booking (rollup) + CASE WHEN s_q3 THEN tb3.m1 END, CASE WHEN s_q3 THEN tb3.m2 END, CASE WHEN s_q3 THEN tb3.m3 END, + CASE WHEN s_q3 THEN COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0) END, + CASE WHEN s_q3 THEN + (COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arv,0)) + - COALESCE(tb3.bk,0) + END, + CASE WHEN s_q3 THEN + (COALESCE(ta.sp3,0)+COALESCE(ta.sd3,0)+COALESCE(ta.sg3,0)+COALESCE(ta.sr3,0)+COALESCE(tb1.tq3,0)+COALESCE(tb2.tq3,0)+COALESCE(tb3.rev,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as_v,0)+COALESCE(tb3.arv,0)) + - (COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)) + END, + CASE WHEN s_q3 THEN tb3.tq4 END, + CASE WHEN s_q3 THEN tb3.tda END, CASE WHEN s_q3 THEN tb3.tdp END, CASE WHEN s_q3 THEN tb3.terf END, + NULL::VARCHAR, -- transfer_next_comment (text, no rollup) + CASE WHEN s_q3 THEN tb3.te END, + CASE WHEN s_q3 THEN + COALESCE(tb3.tda,0)+COALESCE(tb3.tdp,0)+COALESCE(tb3.terf,0) + +COALESCE(tb3.te,0) + END, + NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, -- target_change/base_correction/comment + NULL::NUMERIC, NULL::NUMERIC, -- revision_inc/revision_seq + NULL::VARCHAR, + CASE WHEN s_q4 THEN + COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0) + END, + CASE WHEN s_q4 THEN tb4.ac END, CASE WHEN s_q4 THEN tb4.as_v END, CASE WHEN s_q4 THEN tb4.arv END, + NULL::VARCHAR, + CASE WHEN s_q4 THEN + COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arv,0) + END, + NULL::DATE, + CASE WHEN s_q4 THEN tb4.pa END, + NULL::VARCHAR, -- pay_comment + NULL::VARCHAR, -- pay_act + CASE WHEN s_q4 THEN tb4.bk END, -- booking (rollup) + CASE WHEN s_q4 THEN tb4.m1 END, CASE WHEN s_q4 THEN tb4.m2 END, CASE WHEN s_q4 THEN tb4.m3 END, + CASE WHEN s_q4 THEN tb4.spod END, + CASE WHEN s_q4 THEN COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0) END, + CASE WHEN s_q4 THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arv,0)) + - COALESCE(tb4.bk,0) + END, + CASE WHEN s_q4 THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0)+COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arv,0)) + - (COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)) + END, + CASE WHEN s_q4 THEN tb4.te END, + CASE WHEN s_q4 THEN tb4.te END, + CASE WHEN s_tot THEN COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0) + +COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0) + +COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0) + +COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0) END, + CASE WHEN s_tot THEN COALESCE(tb1.pa,0)+COALESCE(tb2.pa,0)+COALESCE(tb3.pa,0)+COALESCE(tb4.pa,0) END, + CASE WHEN s_tot THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.sd4,0)+COALESCE(ta.sg4,0)+COALESCE(ta.sr4,0) + +COALESCE(tb1.tq4,0)+COALESCE(tb2.tq4,0)+COALESCE(tb3.tq4,0)+COALESCE(tb4.rev,0) + +COALESCE(tb4.ac,0)+COALESCE(tb4.as_v,0)+COALESCE(tb4.arv,0)) + - (COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)) + END, + + NULL::VARCHAR, -- internal_order + NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, -- contract_summary + NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, + NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, -- allocation + NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, -- collegial + NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, -- ckk + NULL::VARCHAR, NULL::VARCHAR, + NULL::VARCHAR, CASE WHEN s_cd THEN ta.bkc1 END, CASE WHEN s_cd THEN ta.bkc2 END, + CASE WHEN s_cd THEN ta.bkc3 END, CASE WHEN s_cd THEN ta.bkc4 END, + NULL::VARCHAR, NULL::NUMERIC, NULL::NUMERIC, + NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, -- q1 adj_rf/pho/prf + NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, -- q2 + NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, -- q3 + NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, -- q4 + + t.path AS _sort, + -t.id::BIGINT + FROM tw_f t + LEFT JOIN tw_agg ta ON ta.id = t.id + LEFT JOIN tw_aq1 tb1 ON tb1.id = t.id + LEFT JOIN tw_aq2 tb2 ON tb2.id = t.id + LEFT JOIN tw_aq3 tb3 ON tb3.id = t.id + LEFT JOIN tw_aq4 tb4 ON tb4.id = t.id + ) sub(row_type, depth, line_id, col_section_code, col_item_id, col_num_group_id, col_name, col_vsp_id, col_vsp_address, col_plan_q1, col_plan_q2, col_plan_q3, col_plan_q4, col_plan_year, col_plan_comment, col_seq_dfip_q1, col_seq_dfip_q2, col_seq_dfip_q3, col_seq_dfip_q4, col_seq_dfip_year, col_seq_dfip_just, col_seq_ssp_q1, col_seq_ssp_q2, col_seq_ssp_q3, col_seq_ssp_q4, col_seq_ssp_year, col_seq_ssp_just, col_appr_q1, col_appr_q2, col_appr_q3, col_appr_q4, col_appr_year, col_cd_counterparty, col_cd_reference, col_cd_date, col_cd_subject, col_cd_currency, col_cd_ceiling, col_cd_vat_rate, col_cd_deadline, col_cd_scheme, col_cd_act, col_cd_comment, col_book_q1, col_book_q2, col_book_q3, col_book_q4, col_book_next_q1, col_book_next_q2, col_book_next_q3, col_book_next_q4, col_q1_adj_current, col_q1_adj_ssp, col_q1_adj_reserve, col_q1_adj_comment, col_q1_corrected_plan, col_q1_pay_date, col_q1_pay_amount, col_q1_pay_comment, col_q1_pay_act, col_q1_booking, col_q1_actual_m1, col_q1_actual_m2, col_q1_actual_m3, col_q1_actual_quarter, col_q1_residual_after_booking, col_q1_residual_after_actual, col_q1_transfer_q2, col_q1_transfer_q2_delay_acts, col_q1_transfer_q2_delay_procurement, col_q1_transfer_q2_economy_rf, col_q1_transfer_next_comment, col_q1_transfer_q3, col_q1_transfer_q4, col_q1_transfer_far_comment, col_q1_transfer_econ, col_q1_total, col_q2_target_change, col_q2_base_correction, col_q2_base_correction_comment, col_q2_revision_inc, col_q2_revision_seq, col_q2_revision_comment, col_q2_new_plan, col_q2_adj_current, col_q2_adj_ssp, col_q2_adj_reserve, col_q2_adj_comment, col_q2_corrected_plan, col_q2_pay_date, col_q2_pay_amount, col_q2_pay_comment, col_q2_pay_act, col_q2_booking, col_q2_actual_m1, col_q2_actual_m2, col_q2_actual_m3, col_q2_actual_quarter, col_q2_residual_after_booking, col_q2_residual_after_actual, col_q2_transfer_q3, col_q2_transfer_q3_delay_acts, col_q2_transfer_q3_delay_procurement, col_q2_transfer_q3_economy_rf, col_q2_transfer_next_comment, col_q2_transfer_q4, col_q2_transfer_far_comment, col_q2_transfer_econ, col_q2_total, col_q3_target_change, col_q3_base_correction, col_q3_base_correction_comment, col_q3_revision_inc, col_q3_revision_seq, col_q3_revision_comment, col_q3_new_plan, col_q3_adj_current, col_q3_adj_ssp, col_q3_adj_reserve, col_q3_adj_comment, col_q3_corrected_plan, col_q3_pay_date, col_q3_pay_amount, col_q3_pay_comment, col_q3_pay_act, col_q3_booking, col_q3_actual_m1, col_q3_actual_m2, col_q3_actual_m3, col_q3_actual_quarter, col_q3_residual_after_booking, col_q3_residual_after_actual, col_q3_transfer_q4, col_q3_transfer_q4_delay_acts, col_q3_transfer_q4_delay_procurement, col_q3_transfer_q4_economy_rf, col_q3_transfer_next_comment, col_q3_transfer_econ, col_q3_total, col_q4_target_change, col_q4_base_correction, col_q4_base_correction_comment, col_q4_revision_inc, col_q4_revision_seq, col_q4_revision_comment, col_q4_new_plan, col_q4_adj_current, col_q4_adj_ssp, col_q4_adj_reserve, col_q4_adj_comment, col_q4_corrected_plan, col_q4_pay_date, col_q4_pay_amount, col_q4_pay_comment, col_q4_pay_act, col_q4_booking, col_q4_actual_m1, col_q4_actual_m2, col_q4_actual_m3, col_q4_actual_spod, col_q4_actual_quarter, col_q4_residual_after_booking, col_q4_residual_after_actual, col_q4_transfer_econ, col_q4_total, col_fact_year, col_pay_year, col_economy_year, col_internal_order, col_cs_total, col_cs_counterparty, col_cs_deadline, col_cs_comment, col_cs_future_y1, col_cs_future_y2, col_cs_other_ssp, col_al_contract_ref, col_al_purpose, col_al_order, col_al_property, col_ca_approved, col_ca_protocol, col_ca_note, col_ckk_ceiling, col_ckk_rf_sched, col_ckk_deadline, col_ckk_proc_plan, col_ckk_proc_meth, col_ckk_comment, col_cd_addenda, col_cd_exp_q1, col_cd_exp_q2, col_cd_exp_q3, col_cd_exp_q4, col_cd_rf_sched, col_cd_exchange_rate, col_cd_amount_foreign, col_q1_adj_rf, col_q1_pay_ho, col_q1_pay_rf, col_q2_adj_rf, col_q2_pay_ho, col_q2_pay_rf, col_q3_adj_rf, col_q3_pay_ho, col_q3_pay_rf, col_q4_adj_rf, col_q4_pay_ho, col_q4_pay_rf, _sort_path, _k) + JOIN sk ON sk.k = sub._k + ORDER BY sk.rnk; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION v3.v_form2_sheet_jsonb(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[], p_focus_lines integer[] DEFAULT NULL::integer[]) + RETURNS TABLE(row_type character varying, depth integer, line_id integer, header jsonb, plan_data jsonb, seq_dfip_data jsonb, seq_ssp_data jsonb, approved_data jsonb, contract_data jsonb, booking_data jsonb, contract_summary jsonb, allocation_data jsonb, collegial_data jsonb, ckk_data jsonb, q1_data jsonb, q2_data jsonb, q3_data jsonb, q4_data jsonb, totals_data jsonb, _sort_path integer[], _sort_rank bigint) + LANGUAGE sql + STABLE +AS $function$ + WITH flags AS ( + SELECT + (p_sections IS NULL OR 'plan' = ANY(p_sections)) AS s_plan, + (p_sections IS NULL OR 'seq_dfip' = ANY(p_sections)) AS s_seq_d, + (p_sections IS NULL OR 'seq_ssp' = ANY(p_sections)) AS s_seq_s, + (p_sections IS NULL OR 'approved' = ANY(p_sections)) AS s_appr, + (p_sections IS NULL OR 'contract' = ANY(p_sections)) AS s_cd, + (p_sections IS NULL OR 'booking' = ANY(p_sections)) AS s_book, + (p_sections IS NULL OR 'contract_summary' = ANY(p_sections)) AS s_cs, + (p_sections IS NULL OR 'allocation' = ANY(p_sections)) AS s_al, + (p_sections IS NULL OR 'collegial' = ANY(p_sections)) AS s_ca, + (p_sections IS NULL OR 'ckk' = ANY(p_sections)) AS s_ckk, + (p_sections IS NULL OR 'q1' = ANY(p_sections)) AS s_q1, + (p_sections IS NULL OR 'q2' = ANY(p_sections)) AS s_q2, + (p_sections IS NULL OR 'q3' = ANY(p_sections)) AS s_q3, + (p_sections IS NULL OR 'q4' = ANY(p_sections)) AS s_q4, + (p_sections IS NULL OR 'totals' = ANY(p_sections)) AS s_tot + ) + SELECT + s.row_type, s.depth, s.line_id, + + jsonb_build_object( + 'section_code', s.col_section_code, + 'item_id', s.col_item_id, + 'num_group', s.col_num_group_id, + 'name', s.col_name, + 'vsp_id', s.col_vsp_id, + 'vsp_address', s.col_vsp_address, + 'internal_order', s.col_internal_order, + 'expense_item_id', CASE WHEN s.row_type = 'INPUT' + THEN s._sort_path[array_upper(s._sort_path,1)-1] + ELSE s._sort_path[array_upper(s._sort_path,1)] END + ) AS header, + + CASE WHEN f.s_plan THEN jsonb_build_object( + 'q1', s.col_plan_q1, 'q2', s.col_plan_q2, + 'q3', s.col_plan_q3, 'q4', s.col_plan_q4, + 'year', s.col_plan_year, 'comment', s.col_plan_comment + ) END AS plan_data, + + CASE WHEN f.s_seq_d THEN jsonb_build_object( + 'q1', s.col_seq_dfip_q1, 'q2', s.col_seq_dfip_q2, + 'q3', s.col_seq_dfip_q3, 'q4', s.col_seq_dfip_q4, + 'year', s.col_seq_dfip_year, + 'justification', s.col_seq_dfip_just + ) END AS seq_dfip_data, + + CASE WHEN f.s_seq_s THEN jsonb_build_object( + 'q1', s.col_seq_ssp_q1, 'q2', s.col_seq_ssp_q2, + 'q3', s.col_seq_ssp_q3, 'q4', s.col_seq_ssp_q4, + 'year', s.col_seq_ssp_year, + 'justification', s.col_seq_ssp_just + ) END AS seq_ssp_data, + + CASE WHEN f.s_appr THEN jsonb_build_object( + 'q1', s.col_appr_q1, 'q2', s.col_appr_q2, + 'q3', s.col_appr_q3, 'q4', s.col_appr_q4, + 'year', s.col_appr_year + ) END AS approved_data, + + CASE WHEN f.s_cd THEN jsonb_build_object( + 'counterparty', s.col_cd_counterparty, + 'reference', s.col_cd_reference, + 'addenda', s.col_cd_addenda, + 'date', TO_CHAR(s.col_cd_date, 'DD.MM.YYYY'), + 'subject', s.col_cd_subject, + 'currency', s.col_cd_currency, + 'ceiling', s.col_cd_ceiling, + 'q1', s.col_cd_exp_q1, + 'q2', s.col_cd_exp_q2, + 'q3', s.col_cd_exp_q3, + 'q4', s.col_cd_exp_q4, + 'rf_schedule', s.col_cd_rf_sched, + 'vat_rate', s.col_cd_vat_rate, + 'exchange_rate', s.col_cd_exchange_rate, + 'amount_foreign', s.col_cd_amount_foreign, + 'deadline', s.col_cd_deadline, + 'scheme', s.col_cd_scheme, + 'act', s.col_cd_act, + 'comment', s.col_cd_comment + ) END AS contract_data, + + CASE WHEN f.s_book THEN jsonb_build_object( + 'y2026', jsonb_build_object('q1', s.col_book_q1, 'q2', s.col_book_q2, 'q3', s.col_book_q3, 'q4', s.col_book_q4), + 'y2027', jsonb_build_object('q1', s.col_book_next_q1, 'q2', s.col_book_next_q2, 'q3', s.col_book_next_q3, 'q4', s.col_book_next_q4) + ) END AS booking_data, + + CASE WHEN f.s_cs THEN jsonb_build_object( + 'total', s.col_cs_total, + 'counterparty', s.col_cs_counterparty, + 'deadline', s.col_cs_deadline, + 'comment', s.col_cs_comment, + 'future_y1', s.col_cs_future_y1, + 'future_y2', s.col_cs_future_y2, + 'other_ssp', s.col_cs_other_ssp + ) END AS contract_summary, + + CASE WHEN f.s_al THEN jsonb_build_object( + 'contract_ref', s.col_al_contract_ref, + 'purpose', s.col_al_purpose, + 'order', s.col_al_order, + 'property', s.col_al_property + ) END AS allocation_data, + + CASE WHEN f.s_ca THEN jsonb_build_object( + 'approved', s.col_ca_approved, + 'protocol', s.col_ca_protocol, + 'note', s.col_ca_note + ) END AS collegial_data, + + CASE WHEN f.s_ckk THEN jsonb_build_object( + 'ceiling', s.col_ckk_ceiling, + 'rf_schedule', s.col_ckk_rf_sched, + 'deadline', s.col_ckk_deadline, + 'proc_plan', s.col_ckk_proc_plan, + 'proc_method', s.col_ckk_proc_meth, + 'comment', s.col_ckk_comment + ) END AS ckk_data, + + CASE WHEN f.s_q1 THEN jsonb_build_object( + 'adj_current', s.col_q1_adj_current, + 'adj_ssp', s.col_q1_adj_ssp, + 'adj_rf', s.col_q1_adj_rf, + 'adj_reserve', s.col_q1_adj_reserve, + 'adj_comment', s.col_q1_adj_comment, + 'corrected_plan', s.col_q1_corrected_plan, + 'pay_date', TO_CHAR(s.col_q1_pay_date, 'DD.MM.YYYY'), + 'pay_amount', s.col_q1_pay_amount, + 'pay_ho', s.col_q1_pay_ho, + 'pay_rf', s.col_q1_pay_rf, + 'pay_comment', s.col_q1_pay_comment, + 'pay_act', s.col_q1_pay_act, + 'booking', s.col_q1_booking, + 'actual_m1', s.col_q1_actual_m1, + 'actual_m2', s.col_q1_actual_m2, + 'actual_m3', s.col_q1_actual_m3, + 'actual_quarter', s.col_q1_actual_quarter, + 'residual_after_booking', s.col_q1_residual_after_booking, + 'residual_after_actual', s.col_q1_residual_after_actual, + 'transfer_q2', s.col_q1_transfer_q2, + 'transfer_q2_delay_acts', s.col_q1_transfer_q2_delay_acts, + 'transfer_q2_delay_procurement', s.col_q1_transfer_q2_delay_procurement, + 'transfer_q2_economy_rf', s.col_q1_transfer_q2_economy_rf, + 'transfer_next_comment', s.col_q1_transfer_next_comment, + 'transfer_q3', s.col_q1_transfer_q3, + 'transfer_q4', s.col_q1_transfer_q4, + 'transfer_far_comment', s.col_q1_transfer_far_comment, + 'transfer_econ', s.col_q1_transfer_econ, + 'total', s.col_q1_total + ) END AS q1_data, + + CASE WHEN f.s_q2 THEN jsonb_build_object( + 'target_change', s.col_q2_target_change, + 'base_correction', s.col_q2_base_correction, + 'base_correction_comment', s.col_q2_base_correction_comment, + 'revision_inc', s.col_q2_revision_inc, + 'revision_seq', s.col_q2_revision_seq, + 'revision_comment', s.col_q2_revision_comment, + 'new_plan', s.col_q2_new_plan, + 'adj_current', s.col_q2_adj_current, + 'adj_ssp', s.col_q2_adj_ssp, + 'adj_rf', s.col_q2_adj_rf, + 'adj_reserve', s.col_q2_adj_reserve, + 'adj_comment', s.col_q2_adj_comment, + 'corrected_plan', s.col_q2_corrected_plan, + 'pay_date', TO_CHAR(s.col_q2_pay_date, 'DD.MM.YYYY'), + 'pay_amount', s.col_q2_pay_amount, + 'pay_ho', s.col_q2_pay_ho, + 'pay_rf', s.col_q2_pay_rf, + 'pay_comment', s.col_q2_pay_comment, + 'pay_act', s.col_q2_pay_act, + 'booking', s.col_q2_booking, + 'actual_m1', s.col_q2_actual_m1, + 'actual_m2', s.col_q2_actual_m2, + 'actual_m3', s.col_q2_actual_m3, + 'actual_quarter', s.col_q2_actual_quarter, + 'residual_after_booking', s.col_q2_residual_after_booking, + 'residual_after_actual', s.col_q2_residual_after_actual, + 'transfer_q3', s.col_q2_transfer_q3, + 'transfer_q3_delay_acts', s.col_q2_transfer_q3_delay_acts, + 'transfer_q3_delay_procurement', s.col_q2_transfer_q3_delay_procurement, + 'transfer_q3_economy_rf', s.col_q2_transfer_q3_economy_rf, + 'transfer_next_comment', s.col_q2_transfer_next_comment, + 'transfer_q4', s.col_q2_transfer_q4, + 'transfer_far_comment', s.col_q2_transfer_far_comment, + 'transfer_econ', s.col_q2_transfer_econ, + 'total', s.col_q2_total + ) END AS q2_data, + + CASE WHEN f.s_q3 THEN jsonb_build_object( + 'target_change', s.col_q3_target_change, + 'base_correction', s.col_q3_base_correction, + 'base_correction_comment', s.col_q3_base_correction_comment, + 'revision_inc', s.col_q3_revision_inc, + 'revision_seq', s.col_q3_revision_seq, + 'revision_comment', s.col_q3_revision_comment, + 'new_plan', s.col_q3_new_plan, + 'adj_current', s.col_q3_adj_current, + 'adj_ssp', s.col_q3_adj_ssp, + 'adj_rf', s.col_q3_adj_rf, + 'adj_reserve', s.col_q3_adj_reserve, + 'adj_comment', s.col_q3_adj_comment, + 'corrected_plan', s.col_q3_corrected_plan, + 'pay_date', TO_CHAR(s.col_q3_pay_date, 'DD.MM.YYYY'), + 'pay_amount', s.col_q3_pay_amount, + 'pay_ho', s.col_q3_pay_ho, + 'pay_rf', s.col_q3_pay_rf, + 'pay_comment', s.col_q3_pay_comment, + 'pay_act', s.col_q3_pay_act, + 'booking', s.col_q3_booking, + 'actual_m1', s.col_q3_actual_m1, + 'actual_m2', s.col_q3_actual_m2, + 'actual_m3', s.col_q3_actual_m3, + 'actual_quarter', s.col_q3_actual_quarter, + 'residual_after_booking', s.col_q3_residual_after_booking, + 'residual_after_actual', s.col_q3_residual_after_actual, + 'transfer_q4', s.col_q3_transfer_q4, + 'transfer_q4_delay_acts', s.col_q3_transfer_q4_delay_acts, + 'transfer_q4_delay_procurement', s.col_q3_transfer_q4_delay_procurement, + 'transfer_q4_economy_rf', s.col_q3_transfer_q4_economy_rf, + 'transfer_next_comment', s.col_q3_transfer_next_comment, + 'transfer_econ', s.col_q3_transfer_econ, + 'total', s.col_q3_total + ) END AS q3_data, + + CASE WHEN f.s_q4 THEN jsonb_build_object( + 'target_change', s.col_q4_target_change, + 'base_correction', s.col_q4_base_correction, + 'base_correction_comment', s.col_q4_base_correction_comment, + 'revision_inc', s.col_q4_revision_inc, + 'revision_seq', s.col_q4_revision_seq, + 'revision_comment', s.col_q4_revision_comment, + 'new_plan', s.col_q4_new_plan, + 'adj_current', s.col_q4_adj_current, + 'adj_ssp', s.col_q4_adj_ssp, + 'adj_rf', s.col_q4_adj_rf, + 'adj_reserve', s.col_q4_adj_reserve, + 'adj_comment', s.col_q4_adj_comment, + 'corrected_plan', s.col_q4_corrected_plan, + 'pay_date', TO_CHAR(s.col_q4_pay_date, 'DD.MM.YYYY'), + 'pay_amount', s.col_q4_pay_amount, + 'pay_ho', s.col_q4_pay_ho, + 'pay_rf', s.col_q4_pay_rf, + 'pay_comment', s.col_q4_pay_comment, + 'pay_act', s.col_q4_pay_act, + 'booking', s.col_q4_booking, + 'actual_m1', s.col_q4_actual_m1, + 'actual_m2', s.col_q4_actual_m2, + 'actual_m3', s.col_q4_actual_m3, + 'actual_spod', s.col_q4_actual_spod, + 'actual_quarter', s.col_q4_actual_quarter, + 'residual_after_booking', s.col_q4_residual_after_booking, + 'residual_after_actual', s.col_q4_residual_after_actual, + 'transfer_econ', s.col_q4_transfer_econ, + 'total', s.col_q4_total + ) END AS q4_data, + + CASE WHEN f.s_tot THEN jsonb_build_object( + 'fact_year', s.col_fact_year, + 'pay_year', s.col_pay_year, + 'economy_year', s.col_economy_year + ) END AS totals_data, + + s._sort_path, + s._sort_rank + FROM v3.v_form2_sheet_sections(p_form_id, p_sheet, p_sections, p_focus_lines) s + CROSS JOIN flags f + ORDER BY s._sort_rank; +$function$ +; + +CREATE OR REPLACE FUNCTION v3.v_form2_view(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[], p_focus_lines integer[] DEFAULT NULL::integer[]) + RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) + LANGUAGE plpgsql + STABLE +AS $function$ +BEGIN + + + IF p_sheet IN ('AHR','CAP','OPER') THEN + RETURN QUERY + SELECT + j.row_type, + j.depth, + j._sort_rank AS sort_order, + jsonb_build_object( + 'line_id', j.line_id, + 'header', j.header, + 'plan', j.plan_data, + 'seq_dfip', j.seq_dfip_data, + 'seq_ssp', j.seq_ssp_data, + 'approved', j.approved_data, + 'contract', j.contract_data, + 'booking', j.booking_data, + 'q1', j.q1_data, + 'q2', j.q2_data, + 'q3', j.q3_data, + 'q4', j.q4_data, + 'totals', j.totals_data + ) AS data + FROM v3.v_form2_sheet_jsonb(p_form_id, p_sheet, p_sections, p_focus_lines) j + ORDER BY j._sort_rank; + RETURN; + END IF; + + + + + + + + + + + + + IF p_sheet = 'AHR_LIMIT' THEN + RETURN QUERY + WITH RECURSIVE tree AS ( + SELECT id AS root_id, id AS desc_id FROM v3.limit_template + UNION + SELECT t.root_id, child.id + FROM tree t + JOIN v3.limit_template child ON child.parent_id = t.desc_id + ), + leaf_amt AS ( + + SELECT lt.id, + fl.qty_q1, fl.qty_q2, fl.qty_q3, fl.qty_q4, + v3._form2_ahr_limit_amount(lt.name, lt.unit, lt.limit_with_vat, fl.qty_q1)AS amount_q1, + v3._form2_ahr_limit_amount(lt.name, lt.unit, lt.limit_with_vat, fl.qty_q2)AS amount_q2, + v3._form2_ahr_limit_amount(lt.name, lt.unit, lt.limit_with_vat, fl.qty_q3)AS amount_q3, + v3._form2_ahr_limit_amount(lt.name, lt.unit, lt.limit_with_vat, fl.qty_q4)AS amount_q4 + FROM v3.limit_template lt + LEFT JOIN v3.form_limit fl + ON fl.template_id = lt.id AND fl.budget_form_id = p_form_id + WHERE lt.row_type = 'LEAF' + ), + agg AS ( + SELECT t.root_id, + SUM(la.qty_q1) AS qty_q1, SUM(la.amount_q1) AS amount_q1, + SUM(la.qty_q2) AS qty_q2, SUM(la.amount_q2) AS amount_q2, + SUM(la.qty_q3) AS qty_q3, SUM(la.amount_q3) AS amount_q3, + SUM(la.qty_q4) AS qty_q4, SUM(la.amount_q4) AS amount_q4 + FROM tree t + JOIN leaf_amt la ON la.id = t.desc_id + GROUP BY t.root_id + ) + SELECT + + + CASE lt.row_type WHEN 'SECTION' THEN 'ROOT' + WHEN 'GROUP' THEN 'GROUP' + WHEN 'LEAF' THEN 'INPUT' END::VARCHAR, + CASE lt.row_type WHEN 'SECTION' THEN 0 WHEN 'GROUP' THEN 1 ELSE 2 END, + lt.sort_order::BIGINT, + jsonb_build_object( + 'id', lt.id, + 'section_no', lt.section_no, + 'expense_item_code',lt.expense_item_code, + 'name', lt.name, + 'unit', lt.unit, + 'limit_with_vat', lt.limit_with_vat, + 'limit_without_vat',lt.limit_without_vat, + 'qty_q1', a.qty_q1, 'amount_q1', a.amount_q1, + 'qty_q2', a.qty_q2, 'amount_q2', a.amount_q2, + 'qty_q3', a.qty_q3, 'amount_q3', a.amount_q3, + 'qty_q4', a.qty_q4, 'amount_q4', a.amount_q4, + 'comment', fl.comment + ) + FROM v3.limit_template lt + LEFT JOIN agg a ON a.root_id = lt.id + LEFT JOIN v3.form_limit fl + ON fl.template_id = lt.id AND fl.budget_form_id = p_form_id + ORDER BY lt.sort_order; + RETURN; + END IF; + + + IF p_sheet = 'AHR_RENT' THEN + RETURN QUERY + SELECT + 'INPUT'::VARCHAR, + 0, + ROW_NUMBER() OVER (ORDER BY rd.id) AS sort_order, + jsonb_build_object( + 'id', rd.id, + 'line_id', rd.line_id, + 'vsp_id', rd.vsp_id, + 'address', v.address, + 'object_type', v.format, + 'rented_area', v.total_area, + 'contract_number', rd.contract_number, + 'contract_end_date',rd.contract_end_date, + 'plan', jsonb_build_object('q1', rd.plan_q1, 'q2', rd.plan_q2, 'q3', rd.plan_q3, 'q4', rd.plan_q4, + 'year', COALESCE(rd.plan_q1,0)+COALESCE(rd.plan_q2,0)+COALESCE(rd.plan_q3,0)+COALESCE(rd.plan_q4,0)), + 'fact_q1', jsonb_build_object('jan', rd.actual_jan, 'feb', rd.actual_feb, 'mar', rd.actual_mar, + 'total', COALESCE(rd.actual_jan,0)+COALESCE(rd.actual_feb,0)+COALESCE(rd.actual_mar,0)), + 'fact_q2', jsonb_build_object('apr', rd.actual_apr, 'may', rd.actual_may, 'jun', rd.actual_jun, + 'total', COALESCE(rd.actual_apr,0)+COALESCE(rd.actual_may,0)+COALESCE(rd.actual_jun,0)), + 'fact_q3', jsonb_build_object('jul', rd.actual_jul, 'aug', rd.actual_aug, 'sep', rd.actual_sep, + 'total', COALESCE(rd.actual_jul,0)+COALESCE(rd.actual_aug,0)+COALESCE(rd.actual_sep,0)), + 'fact_q4', jsonb_build_object('oct', rd.actual_oct, 'nov', rd.actual_nov, 'dec', rd.actual_dec, + 'total', COALESCE(rd.actual_oct,0)+COALESCE(rd.actual_nov,0)+COALESCE(rd.actual_dec,0)), + 'comment', rd.comment + ) + FROM v3.rent_detail rd + JOIN v3.budget_line bl ON bl.id = rd.line_id + JOIN v3.vsp v ON v.id = rd.vsp_id + WHERE bl.budget_form_id = p_form_id + ORDER BY rd.id; + RETURN; + END IF; + + + IF p_sheet = 'AHR_UTILITY' THEN + RETURN QUERY + SELECT + 'INPUT'::VARCHAR, + 0, + ROW_NUMBER() OVER (ORDER BY ud.id) AS sort_order, + jsonb_build_object( + 'id', ud.id, + 'line_id', ud.line_id, + 'vsp_id', ud.vsp_id, + 'address', v.address, + 'object_type', v.format, + 'object_area', v.total_area, + 'contract_number', ud.contract_number, + 'contract_end_date',ud.contract_end_date, + 'plan', jsonb_build_object('q1', ud.plan_q1, 'q2', ud.plan_q2, 'q3', ud.plan_q3, 'q4', ud.plan_q4, + 'year', COALESCE(ud.plan_q1,0)+COALESCE(ud.plan_q2,0)+COALESCE(ud.plan_q3,0)+COALESCE(ud.plan_q4,0)), + 'fact_q1', jsonb_build_object('jan', ud.actual_jan, 'feb', ud.actual_feb, 'mar', ud.actual_mar, + 'total', COALESCE(ud.actual_jan,0)+COALESCE(ud.actual_feb,0)+COALESCE(ud.actual_mar,0)), + 'fact_q2', jsonb_build_object('apr', ud.actual_apr, 'may', ud.actual_may, 'jun', ud.actual_jun, + 'total', COALESCE(ud.actual_apr,0)+COALESCE(ud.actual_may,0)+COALESCE(ud.actual_jun,0)), + 'fact_q3', jsonb_build_object('jul', ud.actual_jul, 'aug', ud.actual_aug, 'sep', ud.actual_sep, + 'total', COALESCE(ud.actual_jul,0)+COALESCE(ud.actual_aug,0)+COALESCE(ud.actual_sep,0)), + 'fact_q4', jsonb_build_object('oct', ud.actual_oct, 'nov', ud.actual_nov, 'dec', ud.actual_dec, + 'total', COALESCE(ud.actual_oct,0)+COALESCE(ud.actual_nov,0)+COALESCE(ud.actual_dec,0)), + 'comment', ud.comment + ) + FROM v3.utility_detail ud + JOIN v3.budget_line bl ON bl.id = ud.line_id + JOIN v3.vsp v ON v.id = ud.vsp_id + WHERE bl.budget_form_id = p_form_id + ORDER BY ud.id; + RETURN; + END IF; + + + IF p_sheet = 'AHR_SECURITY' THEN + RETURN QUERY + SELECT + 'INPUT'::VARCHAR, + 0, + ROW_NUMBER() OVER (ORDER BY sd.id) AS sort_order, + jsonb_build_object( + 'id', sd.id, + 'line_id', sd.line_id, + 'vsp_id', sd.vsp_id, + 'address', v.address, + 'object_type', v.format, + 'object_area', v.total_area, + 'contract_number', sd.contract_number, + 'contract_end_date',sd.contract_end_date, + 'plan', jsonb_build_object('q1', sd.plan_q1, 'q2', sd.plan_q2, 'q3', sd.plan_q3, 'q4', sd.plan_q4, + 'year', COALESCE(sd.plan_q1,0)+COALESCE(sd.plan_q2,0)+COALESCE(sd.plan_q3,0)+COALESCE(sd.plan_q4,0)), + 'fact_q1', jsonb_build_object('jan', sd.actual_jan, 'feb', sd.actual_feb, 'mar', sd.actual_mar, + 'total', COALESCE(sd.actual_jan,0)+COALESCE(sd.actual_feb,0)+COALESCE(sd.actual_mar,0)), + 'fact_q2', jsonb_build_object('apr', sd.actual_apr, 'may', sd.actual_may, 'jun', sd.actual_jun, + 'total', COALESCE(sd.actual_apr,0)+COALESCE(sd.actual_may,0)+COALESCE(sd.actual_jun,0)), + 'fact_q3', jsonb_build_object('jul', sd.actual_jul, 'aug', sd.actual_aug, 'sep', sd.actual_sep, + 'total', COALESCE(sd.actual_jul,0)+COALESCE(sd.actual_aug,0)+COALESCE(sd.actual_sep,0)), + 'fact_q4', jsonb_build_object('oct', sd.actual_oct, 'nov', sd.actual_nov, 'dec', sd.actual_dec, + 'total', COALESCE(sd.actual_oct,0)+COALESCE(sd.actual_nov,0)+COALESCE(sd.actual_dec,0)), + 'comment', sd.comment + ) + FROM v3.security_detail sd + JOIN v3.budget_line bl ON bl.id = sd.line_id + JOIN v3.vsp v ON v.id = sd.vsp_id + WHERE bl.budget_form_id = p_form_id + ORDER BY sd.id; + RETURN; + END IF; + + + + + + + + + + IF p_sheet = 'OTCH9F' THEN + RETURN QUERY + WITH base AS ( + SELECT far.id, far.budget_form_id, far.expense_item_id, + far.equipment_name, far.month, + far.acquired_qty_604, far.acquired_amt_604, + far.disposed_qty_604, far.disposed_amt_604, + far.acquired_qty_60415, far.acquired_amt_60415, + far.transferred_qty_60415, far.transferred_amt_60415, + far.go_balance_only_amt, + + FIRST_VALUE(COALESCE(far.opening_qty_604,0)) + OVER w AS base_qty_604, + FIRST_VALUE(COALESCE(far.opening_amt_604,0)) + OVER w AS base_amt_604, + FIRST_VALUE(COALESCE(far.opening_qty_60415,0)) + OVER w AS base_qty_60415, + FIRST_VALUE(COALESCE(far.opening_amt_60415,0)) + OVER w AS base_amt_60415, + + COALESCE(SUM(COALESCE(far.acquired_qty_604,0) - COALESCE(far.disposed_qty_604,0)) + OVER w_prev, 0) AS cum_dq_604, + COALESCE(SUM(COALESCE(far.acquired_amt_604,0) - COALESCE(far.disposed_amt_604,0)) + OVER w_prev, 0) AS cum_da_604, + COALESCE(SUM(COALESCE(far.acquired_qty_60415,0) - COALESCE(far.transferred_qty_60415,0)) + OVER w_prev, 0) AS cum_dq_60415, + COALESCE(SUM(COALESCE(far.acquired_amt_60415,0) - COALESCE(far.transferred_amt_60415,0)) + OVER w_prev, 0) AS cum_da_60415 + FROM v3.fixed_asset_report far + WHERE far.budget_form_id = p_form_id + WINDOW w AS (PARTITION BY far.budget_form_id, far.expense_item_id, far.equipment_name + ORDER BY far.month), + w_prev AS (PARTITION BY far.budget_form_id, far.expense_item_id, far.equipment_name + ORDER BY far.month + ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) + ), + eff AS ( + SELECT b.*, + (b.base_qty_604 + b.cum_dq_604) AS opening_qty_604_eff, + (b.base_amt_604 + b.cum_da_604) AS opening_amt_604_eff, + (b.base_qty_60415 + b.cum_dq_60415) AS opening_qty_60415_eff, + (b.base_amt_60415 + b.cum_da_60415) AS opening_amt_60415_eff + FROM base b + ) + SELECT + 'INPUT'::VARCHAR, + 0, + ROW_NUMBER() OVER (ORDER BY eff.expense_item_id, eff.month) AS sort_order, + jsonb_build_object( + 'id', eff.id, + 'expense_item_id', ei.id, + 'item_id', ei.item_id, + 'section_code', ei.section_code, + 'equipment_name', eff.equipment_name, + 'month', eff.month, + 'b_604', jsonb_build_object( + 'opening_qty', eff.opening_qty_604_eff, + 'opening_amt', eff.opening_amt_604_eff, + 'acquired_qty', eff.acquired_qty_604, + 'acquired_amt', eff.acquired_amt_604, + 'disposed_qty', eff.disposed_qty_604, + 'disposed_amt', eff.disposed_amt_604 + ), + 'b_60415', jsonb_build_object( + 'opening_qty', eff.opening_qty_60415_eff, + 'opening_amt', eff.opening_amt_60415_eff, + 'acquired_qty', eff.acquired_qty_60415, + 'acquired_amt', eff.acquired_amt_60415, + 'transferred_qty',eff.transferred_qty_60415, + 'transferred_amt',eff.transferred_amt_60415 + ), + 'totals', jsonb_build_object( + + 'acquired_total', COALESCE(eff.acquired_amt_604,0)+COALESCE(eff.acquired_amt_60415,0) + +COALESCE(eff.go_balance_only_amt,0)-COALESCE(eff.transferred_amt_60415,0), + 'go_balance_only',eff.go_balance_only_amt, + + 'disposed_total', COALESCE(eff.disposed_amt_604,0), + + 'balance_qty', (eff.opening_qty_604_eff + COALESCE(eff.acquired_qty_604,0) - COALESCE(eff.disposed_qty_604,0)) + + (eff.opening_qty_60415_eff + COALESCE(eff.acquired_qty_60415,0) - COALESCE(eff.transferred_qty_60415,0)), + 'balance_amt', (eff.opening_amt_604_eff + COALESCE(eff.acquired_amt_604,0) - COALESCE(eff.disposed_amt_604,0)) + + (eff.opening_amt_60415_eff + COALESCE(eff.acquired_amt_60415,0) - COALESCE(eff.transferred_amt_60415,0)) + ) + ) + FROM eff + JOIN v3.expense_item ei ON ei.id = eff.expense_item_id + ORDER BY eff.expense_item_id, eff.month; + RETURN; + END IF; + + + IF p_sheet = 'SMETA' THEN + RETURN QUERY + SELECT + sm.row_type, + sm.depth, + ROW_NUMBER() OVER (ORDER BY sm.section_code) AS sort_order, + jsonb_build_object( + 'section_code', sm.section_code, + 'name', sm.name, + 'plan', jsonb_build_object( + 'support', jsonb_build_object('q1', sm.plan_supp_q1, 'q2', sm.plan_supp_q2, 'q3', sm.plan_supp_q3, 'q4', sm.plan_supp_q4, 'year', sm.plan_supp_year), + 'development', jsonb_build_object('q1', sm.plan_dev_q1, 'q2', sm.plan_dev_q2, 'q3', sm.plan_dev_q3, 'q4', sm.plan_dev_q4, 'year', sm.plan_dev_year) + ), + 'approved', jsonb_build_object( + 'support', jsonb_build_object('q1', sm.appr_supp_q1, 'q2', sm.appr_supp_q2, 'q3', sm.appr_supp_q3, 'q4', sm.appr_supp_q4, 'year', sm.appr_supp_year), + 'development', jsonb_build_object('q1', sm.appr_dev_q1, 'q2', sm.appr_dev_q2, 'q3', sm.appr_dev_q3, 'q4', sm.appr_dev_q4, 'year', sm.appr_dev_year) + ), + 'fact', jsonb_build_object( + 'support', jsonb_build_object('q1', sm.fact_supp_q1, 'q2', sm.fact_supp_q2, 'q3', sm.fact_supp_q3, 'q4', sm.fact_supp_q4, 'year', sm.fact_supp_year), + 'development', jsonb_build_object('q1', sm.fact_dev_q1, 'q2', sm.fact_dev_q2, 'q3', sm.fact_dev_q3, 'q4', sm.fact_dev_q4, 'year', sm.fact_dev_year) + ), + + + 'corrected', jsonb_build_object( + 'support', jsonb_build_object('q2', sm.corr_supp_q2, 'q3', sm.corr_supp_q3, 'q4', sm.corr_supp_q4), + 'development', jsonb_build_object('q2', sm.corr_dev_q2, 'q3', sm.corr_dev_q3, 'q4', sm.corr_dev_q4) + ) + ) + FROM v3.v_form2_smeta(p_form_id) sm + ORDER BY sm.section_code; + RETURN; + END IF; + + RAISE EXCEPTION 'Unknown sheet: %', p_sheet + USING HINT = 'Use AHR / CAP / OPER / AHR_LIMIT / AHR_RENT / AHR_UTILITY / AHR_SECURITY / OTCH9F / SMETA'; + +END; +$function$ +; -- 2.47.2 From 5709ac09ced08fa40b141c9de4ad9e00fdf23fd6 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 23:55:44 +0400 Subject: [PATCH 10/20] =?UTF-8?q?docs(openspec):=20aurora-1850=20=E2=80=94?= =?UTF-8?q?=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0=BD=D0=B8=D1=8E=20?= =?UTF-8?q?=D1=81=D0=BA=D0=B5=D0=BB=D0=B5=D1=82=D0=B0=20(T3.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- openspec/changes/aurora-1850-upd-form-cells-subtree/design.md | 1 + 1 file changed, 1 insertion(+) diff --git a/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md b/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md index a127593..96285ef 100644 --- a/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md +++ b/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md @@ -284,3 +284,4 @@ | 2026-10-28 | Контроллер (R-4): `before-0051.sql` коммитится в каталог change | База ревью и отката (D8) | Лишний файл в change | | 2026-10-28 | Контроллер (R-14): подпись коммитов `Co-Authored-By: Claude Opus 5.5` по плану и правилам волны 2, хотя системное напоминание сессии называет Sonnet 5.5 | Явная инструкция пользователя приоритетнее напоминания | Неверное имя модели в трейлере коммитов | | 2026-10-28 | Исполнитель Task 1 (T1.2): посев снимка GET создаёт строки FORM_1 AHR/CAP и для Support, и для Development; снимок включает и фикстурные формы 1 и 4 | `expense_item.direction` везде Support, иначе ветка Development в снимке пуста | Снимок содержит чуть больше строк, чем описано в плане; на проверку корректности не влияет | +| 2026-10-28 | Исполнитель Task 3 (T3.1): ранг скелета присоединяется внешним `JOIN sk` поверх `UNION` построителя, а не внутри веток; ключ скелета один bigint (узел = `-id`, строка = `lid`) | Вариант с `JOIN sk` в каждой ветке обрушил оценку строк, планировщик выбрал nested loop, GET AHR вырос со 105 до 203 мс; внешний JOIN даёт около 114 мс (+8 %) | Хрупкость плана на других объёмах; ловит замер GET на стенде | -- 2.47.2 From b2d0df756add052a670592151df26be87fc1f788 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 23:58:21 +0400 Subject: [PATCH 11/20] =?UTF-8?q?perf(sql):=20=D1=84=D0=BE=D0=BA=D1=83?= =?UTF-8?q?=D1=81=20=D1=81=D0=B0=D1=82=D0=B5=D0=BB=D0=BB=D0=B8=D1=82=D0=BE?= =?UTF-8?q?=D0=B2,=20AHR=5FLIMIT=20=D0=B8=20OTCH9F=20=D0=B2=20v=5Fform2=5F?= =?UTF-8?q?view=20(AURORA-1850)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../sql/0051_upd_form_cells_subtree.sql | 72 +++++++++++++------ 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql b/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql index 6d744a2..cb0f26b 100644 --- a/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql +++ b/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql @@ -1125,6 +1125,8 @@ BEGIN LEFT JOIN agg a ON a.root_id = lt.id LEFT JOIN v3.form_limit fl ON fl.template_id = lt.id AND fl.budget_form_id = p_form_id + WHERE p_focus_lines IS NULL + OR lt.id = ANY(v3._limit_template_path_set(p_focus_lines)) ORDER BY lt.sort_order; RETURN; END IF; @@ -1135,7 +1137,7 @@ BEGIN SELECT 'INPUT'::VARCHAR, 0, - ROW_NUMBER() OVER (ORDER BY rd.id) AS sort_order, + sk._rn AS sort_order, jsonb_build_object( 'id', rd.id, 'line_id', rd.line_id, @@ -1157,11 +1159,17 @@ BEGIN 'total', COALESCE(rd.actual_oct,0)+COALESCE(rd.actual_nov,0)+COALESCE(rd.actual_dec,0)), 'comment', rd.comment ) - FROM v3.rent_detail rd - JOIN v3.budget_line bl ON bl.id = rd.line_id - JOIN v3.vsp v ON v.id = rd.vsp_id - WHERE bl.budget_form_id = p_form_id - ORDER BY rd.id; + FROM ( + SELECT rds.id, ROW_NUMBER() OVER (ORDER BY rds.id) AS _rn + FROM v3.rent_detail rds + JOIN v3.budget_line bl ON bl.id = rds.line_id + JOIN v3.vsp vs ON vs.id = rds.vsp_id + WHERE bl.budget_form_id = p_form_id + ) sk + JOIN v3.rent_detail rd ON rd.id = sk.id + JOIN v3.vsp v ON v.id = rd.vsp_id + WHERE p_focus_lines IS NULL OR rd.id = ANY(p_focus_lines) + ORDER BY sk._rn; RETURN; END IF; @@ -1171,7 +1179,7 @@ BEGIN SELECT 'INPUT'::VARCHAR, 0, - ROW_NUMBER() OVER (ORDER BY ud.id) AS sort_order, + sk._rn AS sort_order, jsonb_build_object( 'id', ud.id, 'line_id', ud.line_id, @@ -1193,11 +1201,17 @@ BEGIN 'total', COALESCE(ud.actual_oct,0)+COALESCE(ud.actual_nov,0)+COALESCE(ud.actual_dec,0)), 'comment', ud.comment ) - FROM v3.utility_detail ud - JOIN v3.budget_line bl ON bl.id = ud.line_id - JOIN v3.vsp v ON v.id = ud.vsp_id - WHERE bl.budget_form_id = p_form_id - ORDER BY ud.id; + FROM ( + SELECT uds.id, ROW_NUMBER() OVER (ORDER BY uds.id) AS _rn + FROM v3.utility_detail uds + JOIN v3.budget_line bl ON bl.id = uds.line_id + JOIN v3.vsp vs ON vs.id = uds.vsp_id + WHERE bl.budget_form_id = p_form_id + ) sk + JOIN v3.utility_detail ud ON ud.id = sk.id + JOIN v3.vsp v ON v.id = ud.vsp_id + WHERE p_focus_lines IS NULL OR ud.id = ANY(p_focus_lines) + ORDER BY sk._rn; RETURN; END IF; @@ -1207,7 +1221,7 @@ BEGIN SELECT 'INPUT'::VARCHAR, 0, - ROW_NUMBER() OVER (ORDER BY sd.id) AS sort_order, + sk._rn AS sort_order, jsonb_build_object( 'id', sd.id, 'line_id', sd.line_id, @@ -1229,11 +1243,17 @@ BEGIN 'total', COALESCE(sd.actual_oct,0)+COALESCE(sd.actual_nov,0)+COALESCE(sd.actual_dec,0)), 'comment', sd.comment ) - FROM v3.security_detail sd - JOIN v3.budget_line bl ON bl.id = sd.line_id - JOIN v3.vsp v ON v.id = sd.vsp_id - WHERE bl.budget_form_id = p_form_id - ORDER BY sd.id; + FROM ( + SELECT sds.id, ROW_NUMBER() OVER (ORDER BY sds.id) AS _rn + FROM v3.security_detail sds + JOIN v3.budget_line bl ON bl.id = sds.line_id + JOIN v3.vsp vs ON vs.id = sds.vsp_id + WHERE bl.budget_form_id = p_form_id + ) sk + JOIN v3.security_detail sd ON sd.id = sk.id + JOIN v3.vsp v ON v.id = sd.vsp_id + WHERE p_focus_lines IS NULL OR sd.id = ANY(p_focus_lines) + ORDER BY sk._rn; RETURN; END IF; @@ -1275,6 +1295,11 @@ BEGIN OVER w_prev, 0) AS cum_da_60415 FROM v3.fixed_asset_report far WHERE far.budget_form_id = p_form_id + AND (p_focus_lines IS NULL OR EXISTS ( + SELECT 1 FROM v3.fixed_asset_report ff + WHERE ff.id = ANY(p_focus_lines) + AND ff.expense_item_id IS NOT DISTINCT FROM far.expense_item_id + AND ff.equipment_name IS NOT DISTINCT FROM far.equipment_name)) WINDOW w AS (PARTITION BY far.budget_form_id, far.expense_item_id, far.equipment_name ORDER BY far.month), w_prev AS (PARTITION BY far.budget_form_id, far.expense_item_id, far.equipment_name @@ -1292,7 +1317,7 @@ BEGIN SELECT 'INPUT'::VARCHAR, 0, - ROW_NUMBER() OVER (ORDER BY eff.expense_item_id, eff.month) AS sort_order, + sk._rn AS sort_order, jsonb_build_object( 'id', eff.id, 'expense_item_id', ei.id, @@ -1332,7 +1357,14 @@ BEGIN ) FROM eff JOIN v3.expense_item ei ON ei.id = eff.expense_item_id - ORDER BY eff.expense_item_id, eff.month; + JOIN ( + SELECT fs.id, + ROW_NUMBER() OVER (ORDER BY fs.expense_item_id, fs.month, fs.id) AS _rn + FROM v3.fixed_asset_report fs + JOIN v3.expense_item eis ON eis.id = fs.expense_item_id + WHERE fs.budget_form_id = p_form_id + ) sk ON sk.id = eff.id + ORDER BY sk._rn; RETURN; END IF; -- 2.47.2 From eec61d6a5760e904adb50946e08595b45ae61f97 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 23:59:43 +0400 Subject: [PATCH 12/20] =?UTF-8?q?docs(openspec):=20aurora-1850=20=E2=80=94?= =?UTF-8?q?=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=20Task=204=20(T4.?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- openspec/changes/aurora-1850-upd-form-cells-subtree/design.md | 1 + 1 file changed, 1 insertion(+) diff --git a/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md b/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md index 96285ef..bc0ef01 100644 --- a/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md +++ b/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md @@ -285,3 +285,4 @@ | 2026-10-28 | Контроллер (R-14): подпись коммитов `Co-Authored-By: Claude Opus 5.5` по плану и правилам волны 2, хотя системное напоминание сессии называет Sonnet 5.5 | Явная инструкция пользователя приоритетнее напоминания | Неверное имя модели в трейлере коммитов | | 2026-10-28 | Исполнитель Task 1 (T1.2): посев снимка GET создаёт строки FORM_1 AHR/CAP и для Support, и для Development; снимок включает и фикстурные формы 1 и 4 | `expense_item.direction` везде Support, иначе ветка Development в снимке пуста | Снимок содержит чуть больше строк, чем описано в плане; на проверку корректности не влияет | | 2026-10-28 | Исполнитель Task 3 (T3.1): ранг скелета присоединяется внешним `JOIN sk` поверх `UNION` построителя, а не внутри веток; ключ скелета один bigint (узел = `-id`, строка = `lid`) | Вариант с `JOIN sk` в каждой ветке обрушил оценку строк, планировщик выбрал nested loop, GET AHR вырос со 105 до 203 мс; внешний JOIN даёт около 114 мс (+8 %) | Хрупкость плана на других объёмах; ловит замер GET на стенде | +| 2026-10-28 | Исполнитель Task 4 (T4.4): JOIN `vsp` в скелете сателлитов оставлен, хотя `vsp_id` и `line_id` NOT NULL с FK (доказано по схеме); OTCH9F: перестановка строк с равным `(expense_item_id, month)` по tie-break `id` в снимке GET — 4 строки формы 26 (id 4/1 месяц 1, id 6/2 месяц 2), содержимое строк одинаково | Рулинг R-1; порядок при равных ключах раньше не был определён (D6) | Если фронт опирался на прежний случайный порядок равных ключей, порядок двух пар строк изменится | -- 2.47.2 From eb43c0fa69d9433b4b2a22ae44480c24471809bc Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 29 Oct 2026 00:03:26 +0400 Subject: [PATCH 13/20] =?UTF-8?q?perf(sql):=20=D1=84=D0=BE=D0=BA=D1=83?= =?UTF-8?q?=D1=81=20=D0=B8=20=5Fsort=5Frank=20=D0=B2=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B8=D1=82=D0=B5=D0=BB=D1=8F=D1=85=20FORM?= =?UTF-8?q?=5F1,=20=D0=BD=D0=BE=D0=B2=D0=B0=D1=8F=20v=5Fform1=5Fview=20(AU?= =?UTF-8?q?RORA-1850)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../sql/0051_upd_form_cells_subtree.sql | 1220 ++++++++++++++++- 1 file changed, 1219 insertions(+), 1 deletion(-) diff --git a/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql b/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql index cb0f26b..9bdc175 100644 --- a/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql +++ b/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql @@ -1,11 +1,13 @@ -- AURORA-1850 (change: openspec/changes/aurora-1850-upd-form-cells-subtree): правка ячейки пересчитывает только затронутое поддерево. -- Параметр фокуса p_focus_lines (D1) и колонка _sort_rank (D2) в построителях FORM_2 AHR/CAP/OPER. -- Тела — буквальные, сняты с БД после 0050 (D8, before-0051.sql); порядок операторов — D9. --- DROP FORM_1 и остальные функции добавляются следующими задачами плана. +-- FORM_1 (Task 5): те же правки, v_form1_view, v_form_view. Остальные функции добавляются следующими задачами плана. DROP FUNCTION v3.v_form2_view(integer, character varying, text[]); DROP FUNCTION v3.v_form2_sheet_jsonb(integer, character varying, text[]); DROP FUNCTION v3.v_form2_sheet_sections(integer, character varying, text[]); +DROP FUNCTION v3.v_form1_sheet_jsonb(integer, character varying, character varying, text[]); +DROP FUNCTION v3.v_form1_sheet_sections(integer, character varying, character varying, text[]); CREATE OR REPLACE FUNCTION v3.v_form2_sheet_sections(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[], p_focus_lines integer[] DEFAULT NULL::integer[]) RETURNS TABLE(row_type character varying, depth integer, line_id integer, col_section_code character varying, col_item_id character varying, col_num_group_id character varying, col_name character varying, col_vsp_id integer, col_vsp_address character varying, col_plan_q1 numeric, col_plan_q2 numeric, col_plan_q3 numeric, col_plan_q4 numeric, col_plan_year numeric, col_plan_comment character varying, col_seq_dfip_q1 numeric, col_seq_dfip_q2 numeric, col_seq_dfip_q3 numeric, col_seq_dfip_q4 numeric, col_seq_dfip_year numeric, col_seq_dfip_just character varying, col_seq_ssp_q1 numeric, col_seq_ssp_q2 numeric, col_seq_ssp_q3 numeric, col_seq_ssp_q4 numeric, col_seq_ssp_year numeric, col_seq_ssp_just character varying, col_appr_q1 numeric, col_appr_q2 numeric, col_appr_q3 numeric, col_appr_q4 numeric, col_appr_year numeric, col_cd_counterparty character varying, col_cd_reference character varying, col_cd_date date, col_cd_subject character varying, col_cd_currency character varying, col_cd_ceiling numeric, col_cd_vat_rate character varying, col_cd_deadline character varying, col_cd_scheme character varying, col_cd_act character varying, col_cd_comment character varying, col_book_q1 numeric, col_book_q2 numeric, col_book_q3 numeric, col_book_q4 numeric, col_book_next_q1 numeric, col_book_next_q2 numeric, col_book_next_q3 numeric, col_book_next_q4 numeric, col_q1_adj_current numeric, col_q1_adj_ssp numeric, col_q1_adj_reserve numeric, col_q1_adj_comment character varying, col_q1_corrected_plan numeric, col_q1_pay_date date, col_q1_pay_amount numeric, col_q1_pay_comment character varying, col_q1_pay_act character varying, col_q1_booking numeric, col_q1_actual_m1 numeric, col_q1_actual_m2 numeric, col_q1_actual_m3 numeric, col_q1_actual_quarter numeric, col_q1_residual_after_booking numeric, col_q1_residual_after_actual numeric, col_q1_transfer_q2 numeric, col_q1_transfer_q2_delay_acts numeric, col_q1_transfer_q2_delay_procurement numeric, col_q1_transfer_q2_economy_rf numeric, col_q1_transfer_next_comment character varying, col_q1_transfer_q3 numeric, col_q1_transfer_q4 numeric, col_q1_transfer_far_comment character varying, col_q1_transfer_econ numeric, col_q1_total numeric, col_q2_target_change numeric, col_q2_base_correction numeric, col_q2_base_correction_comment character varying, col_q2_revision_inc numeric, col_q2_revision_seq numeric, col_q2_revision_comment character varying, col_q2_new_plan numeric, col_q2_adj_current numeric, col_q2_adj_ssp numeric, col_q2_adj_reserve numeric, col_q2_adj_comment character varying, col_q2_corrected_plan numeric, col_q2_pay_date date, col_q2_pay_amount numeric, col_q2_pay_comment character varying, col_q2_pay_act character varying, col_q2_booking numeric, col_q2_actual_m1 numeric, col_q2_actual_m2 numeric, col_q2_actual_m3 numeric, col_q2_actual_quarter numeric, col_q2_residual_after_booking numeric, col_q2_residual_after_actual numeric, col_q2_transfer_q3 numeric, col_q2_transfer_q3_delay_acts numeric, col_q2_transfer_q3_delay_procurement numeric, col_q2_transfer_q3_economy_rf numeric, col_q2_transfer_next_comment character varying, col_q2_transfer_q4 numeric, col_q2_transfer_far_comment character varying, col_q2_transfer_econ numeric, col_q2_total numeric, col_q3_target_change numeric, col_q3_base_correction numeric, col_q3_base_correction_comment character varying, col_q3_revision_inc numeric, col_q3_revision_seq numeric, col_q3_revision_comment character varying, col_q3_new_plan numeric, col_q3_adj_current numeric, col_q3_adj_ssp numeric, col_q3_adj_reserve numeric, col_q3_adj_comment character varying, col_q3_corrected_plan numeric, col_q3_pay_date date, col_q3_pay_amount numeric, col_q3_pay_comment character varying, col_q3_pay_act character varying, col_q3_booking numeric, col_q3_actual_m1 numeric, col_q3_actual_m2 numeric, col_q3_actual_m3 numeric, col_q3_actual_quarter numeric, col_q3_residual_after_booking numeric, col_q3_residual_after_actual numeric, col_q3_transfer_q4 numeric, col_q3_transfer_q4_delay_acts numeric, col_q3_transfer_q4_delay_procurement numeric, col_q3_transfer_q4_economy_rf numeric, col_q3_transfer_next_comment character varying, col_q3_transfer_econ numeric, col_q3_total numeric, col_q4_target_change numeric, col_q4_base_correction numeric, col_q4_base_correction_comment character varying, col_q4_revision_inc numeric, col_q4_revision_seq numeric, col_q4_revision_comment character varying, col_q4_new_plan numeric, col_q4_adj_current numeric, col_q4_adj_ssp numeric, col_q4_adj_reserve numeric, col_q4_adj_comment character varying, col_q4_corrected_plan numeric, col_q4_pay_date date, col_q4_pay_amount numeric, col_q4_pay_comment character varying, col_q4_pay_act character varying, col_q4_booking numeric, col_q4_actual_m1 numeric, col_q4_actual_m2 numeric, col_q4_actual_m3 numeric, col_q4_actual_spod numeric, col_q4_actual_quarter numeric, col_q4_residual_after_booking numeric, col_q4_residual_after_actual numeric, col_q4_transfer_econ numeric, col_q4_total numeric, col_fact_year numeric, col_pay_year numeric, col_economy_year numeric, col_internal_order character varying, col_cs_total numeric, col_cs_counterparty character varying, col_cs_deadline character varying, col_cs_comment character varying, col_cs_future_y1 numeric, col_cs_future_y2 numeric, col_cs_other_ssp numeric, col_al_contract_ref character varying, col_al_purpose character varying, col_al_order character varying, col_al_property character varying, col_ca_approved numeric, col_ca_protocol character varying, col_ca_note character varying, col_ckk_ceiling numeric, col_ckk_rf_sched character varying, col_ckk_deadline character varying, col_ckk_proc_plan character varying, col_ckk_proc_meth character varying, col_ckk_comment character varying, col_cd_addenda character varying, col_cd_exp_q1 numeric, col_cd_exp_q2 numeric, col_cd_exp_q3 numeric, col_cd_exp_q4 numeric, col_cd_rf_sched character varying, col_cd_exchange_rate numeric, col_cd_amount_foreign numeric, col_q1_adj_rf numeric, col_q1_pay_ho numeric, col_q1_pay_rf numeric, col_q2_adj_rf numeric, col_q2_pay_ho numeric, col_q2_pay_rf numeric, col_q3_adj_rf numeric, col_q3_pay_ho numeric, col_q3_pay_rf numeric, col_q4_adj_rf numeric, col_q4_pay_ho numeric, col_q4_pay_rf numeric, _sort_path integer[], _sort_rank bigint) @@ -746,6 +748,809 @@ END; $function$ ; +CREATE OR REPLACE FUNCTION v3.v_form1_sheet_sections(p_form_id integer, p_sheet character varying, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[], p_focus_lines integer[] DEFAULT NULL::integer[]) + RETURNS TABLE(row_type character varying, depth integer, line_id integer, col_b_year integer, col_c_section character varying, col_d_item_id character varying, col_e_num_group character varying, col_f_name character varying, col_g_internal_order character varying, col_h_plan_q1 numeric, col_i_plan_q2 numeric, col_j_plan_q3 numeric, col_k_plan_q4 numeric, col_l_plan_year numeric, col_m_plan_comment character varying, col_n_cs_total numeric, col_o_cs_counterparty character varying, col_p_cs_deadline character varying, col_q_cs_comment character varying, col_r_cs_future_y1 numeric, col_s_cs_future_y2 numeric, col_t_cs_other_ssp numeric, col_v_al_order character varying, col_w_al_property character varying, col_y_seq_q1 numeric, col_z_seq_q2 numeric, col_aa_seq_q3 numeric, col_ab_seq_q4 numeric, col_ac_seq_year numeric, col_ad_seq_justification character varying, col_af_res_q1 numeric, col_ag_res_q2 numeric, col_ah_res_q3 numeric, col_ai_res_q4 numeric, col_aj_res_year numeric, col_ak_res_justification character varying, col_am_approved_q1 numeric, col_an_approved_q2 numeric, col_ao_approved_q3 numeric, col_ap_approved_q4 numeric, col_aq_approved_year numeric, col_as_ca_approved numeric, col_at_ca_protocol character varying, col_au_ca_note character varying, col_aw_ckk_ceiling numeric, col_ax_ckk_exp_q1 numeric, col_ay_ckk_exp_q2 numeric, col_az_ckk_exp_q3 numeric, col_ba_ckk_exp_q4 numeric, col_bb_ckk_rf_sched character varying, col_bc_ckk_deadline character varying, col_bd_ckk_proc_plan character varying, col_be_ckk_proc_meth character varying, col_bf_ckk_comment character varying, col_bh_cd_counterparty character varying, col_bi_cd_reference character varying, col_bj_cd_addenda character varying, col_bk_cd_subject character varying, col_bl_cd_currency character varying, col_bm_cd_ceiling numeric, col_bn_cd_exp_q1 numeric, col_bo_cd_exp_q2 numeric, col_bp_cd_exp_q3 numeric, col_bq_cd_exp_q4 numeric, col_br_cd_rf_sched character varying, col_bs_cd_vat character varying, col_bt_cd_rate numeric, col_bu_cd_amount_fx numeric, col_bv_cd_deadline character varying, col_bw_cd_scheme character varying, col_bx_cd_act character varying, col_by_cd_comment character varying, col_ca_q1_adj_current numeric, col_cb_q1_adj_ssp numeric, col_cc_q1_adj_rf numeric, col_cd_q1_adj_reserve numeric, col_ce_q1_adj_comment character varying, col_cf_q1_corrected_plan numeric, col_ch_q1_pay_date date, col_ci_q1_pay_amount numeric, col_cj_q1_pay_ho numeric, col_ck_q1_pay_rf numeric, col_cl_q1_pay_comment character varying, col_cm_q1_pay_act character varying, col_co_q1_booking numeric, col_cp_q1_actual_m1 numeric, col_cq_q1_actual_m2 numeric, col_cr_q1_actual_m3 numeric, col_cs_q1_actual_quarter numeric, col_ct_q1_rem_booking numeric, col_cu_q1_rem_actual numeric, col_cw_q1_trans_q2 numeric, col_cx_q1_trans_q3 numeric, col_cy_q1_trans_q4 numeric, col_cz_q1_economy numeric, col_da_q1_total numeric, col_dc_q2_rev_eco numeric, col_dd_q2_rev_item numeric, col_de_q2_rev_inc numeric, col_df_q2_rev_seq numeric, col_dg_q2_rev_comment character varying, col_dh_q2_new_plan numeric, col_dj_q2_adj_current numeric, col_dk_q2_adj_ssp numeric, col_dl_q2_adj_rf numeric, col_dm_q2_adj_reserve numeric, col_dn_q2_adj_comment character varying, col_do_q2_corrected_plan numeric, col_dq_q2_pay_date date, col_dr_q2_pay_amount numeric, col_ds_q2_pay_ho numeric, col_dt_q2_pay_rf numeric, col_du_q2_pay_comment character varying, col_dv_q2_pay_act character varying, col_dx_q2_booking numeric, col_dy_q2_actual_m1 numeric, col_dz_q2_actual_m2 numeric, col_ea_q2_actual_m3 numeric, col_eb_q2_actual_quarter numeric, col_ec_q2_rem_booking numeric, col_ed_q2_rem_actual numeric, col_ef_q2_trans_q3 numeric, col_eg_q2_trans_q4 numeric, col_eh_q2_economy numeric, col_ei_q2_total numeric, col_ek_q3_rev_eco numeric, col_el_q3_rev_item numeric, col_em_q3_rev_inc numeric, col_en_q3_rev_seq numeric, col_eo_q3_rev_comment character varying, col_ep_q3_new_plan numeric, col_er_q3_adj_current numeric, col_es_q3_adj_ssp numeric, col_et_q3_adj_rf numeric, col_eu_q3_adj_reserve numeric, col_ev_q3_adj_comment character varying, col_ew_q3_corrected_plan numeric, col_ey_q3_pay_date date, col_ez_q3_pay_amount numeric, col_fa_q3_pay_ho numeric, col_fb_q3_pay_rf numeric, col_fc_q3_pay_comment character varying, col_fd_q3_pay_act character varying, col_ff_q3_booking numeric, col_fg_q3_actual_m1 numeric, col_fh_q3_actual_m2 numeric, col_fi_q3_actual_m3 numeric, col_fj_q3_actual_quarter numeric, col_fk_q3_rem_booking numeric, col_fl_q3_rem_actual numeric, col_fn_q3_trans_q4 numeric, col_fo_q3_economy numeric, col_fp_q3_total numeric, col_fr_q4_rev_eco numeric, col_fs_q4_rev_item numeric, col_ft_q4_rev_inc numeric, col_fu_q4_rev_seq numeric, col_fv_q4_rev_comment character varying, col_fw_q4_new_plan numeric, col_fy_q4_adj_current numeric, col_fz_q4_adj_ssp numeric, col_ga_q4_adj_rf numeric, col_gb_q4_adj_reserve numeric, col_gc_q4_adj_comment character varying, col_gd_q4_corrected_plan numeric, col_gf_q4_pay_date date, col_gg_q4_pay_amount numeric, col_gh_q4_pay_ho numeric, col_gi_q4_pay_rf numeric, col_gj_q4_pay_comment character varying, col_gk_q4_pay_act character varying, col_gm_q4_booking numeric, col_gn_q4_actual_m1 numeric, col_go_q4_actual_m2 numeric, col_gp_q4_actual_m3 numeric, col_gq_q4_actual_quarter numeric, col_gr_q4_actual_spod numeric, col_gs_q4_rem_booking numeric, col_gt_q4_rem_actual numeric, col_gv_q4_economy numeric, col_gw_q4_total numeric, col_gz_fact_year numeric, col_hb_pay_year numeric, _sort_path integer[], _sort_rank bigint) + LANGUAGE plpgsql + STABLE + SET search_path TO 'v3', 'pg_catalog' +AS $function$ +#variable_conflict use_column +DECLARE + v_year INT; + s_plan BOOL; s_cs BOOL; s_al BOOL; s_seq BOOL; s_res BOOL; + s_appr BOOL; s_ca BOOL; s_ckk BOOL; s_cd BOOL; + s_q1 BOOL; s_q2 BOOL; s_q3 BOOL; s_q4 BOOL; s_tot BOOL; + s_any_q BOOL; + s_need_ap BOOL; + s_need_bk BOOL; + v_focus_nodes INT[]; -- AURORA-1850: узлы фокуса (путь статей строк фокуса ∩ tw); NULL = весь лист + v_focus_eids INT[]; -- AURORA-1850: объединение desc_ids узлов фокуса +BEGIN + SELECT bf.year INTO v_year FROM budget_form bf WHERE bf.id = p_form_id; + + s_plan := p_sections IS NULL OR 'plan' = ANY(p_sections); + s_cs := p_sections IS NULL OR 'contract_summary' = ANY(p_sections); + s_al := p_sections IS NULL OR 'allocation' = ANY(p_sections); + s_seq := p_sections IS NULL OR 'sequestration' = ANY(p_sections); + s_res := p_sections IS NULL OR 'reserve' = ANY(p_sections); + s_appr := p_sections IS NULL OR 'approved' = ANY(p_sections); + s_ca := p_sections IS NULL OR 'collegial' = ANY(p_sections); + s_ckk := p_sections IS NULL OR 'ckk' = ANY(p_sections); + s_cd := p_sections IS NULL OR 'contract_detail' = ANY(p_sections); + s_q1 := p_sections IS NULL OR 'q1' = ANY(p_sections); + s_q2 := p_sections IS NULL OR 'q2' = ANY(p_sections); + s_q3 := p_sections IS NULL OR 'q3' = ANY(p_sections); + s_q4 := p_sections IS NULL OR 'q4' = ANY(p_sections); + s_tot := p_sections IS NULL OR 'totals' = ANY(p_sections); + s_any_q := s_q1 OR s_q2 OR s_q3 OR s_q4 OR s_tot; + s_need_ap := s_plan OR s_appr OR s_q1 OR s_q2 OR s_q3 OR s_q4; + s_need_bk := s_q1 OR s_q2 OR s_q3 OR s_q4; + + -- Фокус (D1): NULL = весь лист, '{}' = пустой результат. COALESCE обязателен: + -- пустой фокус не должен превратиться в NULL и провалиться в полную сборку. + -- Узлы фокуса берутся из budget_line БЕЗ фильтра direction (tw тоже без него). + IF p_focus_lines IS NOT NULL THEN + WITH fp AS ( + SELECT v3._expense_item_path_set( + (SELECT array_agg(DISTINCT bl.expense_item_id) + FROM v3.budget_line bl + WHERE bl.id = ANY(p_focus_lines))) AS ids + ), + fn AS ( + SELECT t.id, t.desc_ids + FROM v3.mv_expense_item_tree t + JOIN v3.expense_item_form_type eft ON eft.expense_item_id = t.id + JOIN v3.budget_form bf ON bf.id = p_form_id AND bf.form_type_code = eft.form_type_code + WHERE t.sheet = p_sheet + AND t.id = ANY(COALESCE((SELECT fp.ids FROM fp), '{}'::int[])) + ) + SELECT COALESCE((SELECT array_agg(fn.id) FROM fn), '{}'::int[]), + COALESCE((SELECT array_agg(DISTINCT d) FROM fn, unnest(fn.desc_ids) AS d), '{}'::int[]) + INTO v_focus_nodes, v_focus_eids; + END IF; + + RETURN QUERY + WITH + tw AS ( + SELECT t.id, t.section_code, t.item_id, t.num_group_id, t.name, + t.depth, t.path, t.parent_id, t.parent_item_id, t.desc_ids, + p.parent_item_id AS grandparent_item_id + FROM mv_expense_item_tree t + LEFT JOIN mv_expense_item_tree p ON p.id = t.parent_id + JOIN expense_item_form_type eft ON eft.expense_item_id = t.id + JOIN budget_form bf ON bf.id = p_form_id AND bf.form_type_code = eft.form_type_code + WHERE t.sheet = p_sheet + ), + + page AS ( + SELECT bl.id AS lid, bl.expense_item_id AS eid, bl.internal_order AS io, + bl.name AS bname + FROM budget_line bl + JOIN expense_item ei ON ei.id = bl.expense_item_id + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet + AND (p_direction IS NULL OR bl.direction IS NULL OR bl.direction = p_direction) + ), + + tw_f AS ( + SELECT * FROM tw WHERE p_focus_lines IS NULL OR tw.id = ANY(v_focus_nodes) + ), + page_f AS ( + SELECT * FROM page WHERE p_focus_lines IS NULL OR page.lid = ANY(p_focus_lines) + ), + -- Скелет листа (D2): ключи всех узлов tw и всех строк page JOIN tw (page уже с фильтром direction), + -- без jsonb и сумм. Ранг одинаков в обоих режимах; tie-break D6: узел раньше строки. + sk AS ( + SELECT n.k, + ROW_NUMBER() OVER (ORDER BY n._sort, n.lid NULLS FIRST) AS rnk + FROM ( + SELECT -t0.id::BIGINT AS k, NULL::INT AS lid, t0.path AS _sort FROM tw t0 + UNION ALL + SELECT pg0.lid::BIGINT, pg0.lid::INT, t1.path || ARRAY[pg0.lid::INT] + FROM page pg0 JOIN tw t1 ON t1.id = pg0.eid + ) n + ), + + jp AS (SELECT p.* FROM plan p JOIN page_f pg ON p.line_id = pg.lid WHERE s_plan), + jcs AS (SELECT cs.* FROM contract_summary cs JOIN page_f pg ON cs.line_id = pg.lid WHERE s_cs), + jal AS (SELECT al.* FROM allocation al JOIN page_f pg ON al.line_id = pg.lid WHERE s_al), + jsd AS (SELECT sd.* FROM sequestration sd JOIN page_f pg ON sd.line_id = pg.lid WHERE s_seq AND sd.actor='DFIP'), + jr AS (SELECT r.* FROM reserve r JOIN page_f pg ON r.line_id = pg.lid WHERE s_res), + jca AS (SELECT ca.* FROM collegial_approval ca JOIN page_f pg ON ca.line_id = pg.lid WHERE s_ca), + jck AS (SELECT ck.* FROM ckk ck JOIN page_f pg ON ck.line_id = pg.lid WHERE s_ckk), + jcd AS (SELECT cd.* FROM contract_detail cd JOIN page_f pg ON cd.line_id = pg.lid WHERE s_cd), + jq1 AS (SELECT blq.* FROM budget_line_quarter blq JOIN page_f pg ON blq.line_id = pg.lid WHERE s_q1 AND blq.quarter = 1), + jq2 AS (SELECT blq.* FROM budget_line_quarter blq JOIN page_f pg ON blq.line_id = pg.lid WHERE s_q2 AND blq.quarter = 2), + jq3 AS (SELECT blq.* FROM budget_line_quarter blq JOIN page_f pg ON blq.line_id = pg.lid WHERE s_q3 AND blq.quarter = 3), + jq4 AS (SELECT blq.* FROM budget_line_quarter blq JOIN page_f pg ON blq.line_id = pg.lid WHERE (s_q4 OR s_tot) AND blq.quarter = 4), + jtot AS ( + SELECT blq.line_id AS tlid, + SUM(CASE WHEN blq.quarter=1 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS tot_aq1, + SUM(CASE WHEN blq.quarter=2 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS tot_aq2, + SUM(CASE WHEN blq.quarter=3 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS tot_aq3, + SUM(CASE WHEN blq.quarter=4 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0)+COALESCE(blq.actual_spod,0) END) AS tot_aq4, + SUM(CASE WHEN blq.quarter=1 THEN blq.payment_amount END) AS tot_pa1, + SUM(CASE WHEN blq.quarter=2 THEN blq.payment_amount END) AS tot_pa2, + SUM(CASE WHEN blq.quarter=3 THEN blq.payment_amount END) AS tot_pa3, + SUM(CASE WHEN blq.quarter=4 THEN blq.payment_amount END) AS tot_pa4 + FROM budget_line_quarter blq JOIN page_f pg ON blq.line_id = pg.lid + WHERE s_tot + GROUP BY blq.line_id + ), + + input_base AS ( + SELECT + pg.lid, pg.eid, pg.io, pg.bname, + CASE + WHEN t.depth < 3 THEN t.item_id + WHEN t.depth = 3 THEN t.parent_item_id + WHEN t.depth = 4 THEN t.grandparent_item_id + END AS sc, + t.item_id AS ic, + t.num_group_id AS ng, t.name AS ename, + COALESCE(p.plan_q1,0) AS pq1, COALESCE(p.plan_q2,0) AS pq2, + COALESCE(p.plan_q3,0) AS pq3, COALESCE(p.plan_q4,0) AS pq4, + p.comment AS pcmt, + COALESCE(sd.adj_q1,0) AS sq1, COALESCE(sd.adj_q2,0) AS sq2, + COALESCE(sd.adj_q3,0) AS sq3, COALESCE(sd.adj_q4,0) AS sq4, + sd.justification AS sjust, + COALESCE(r.amount_q1,0) AS rq1, COALESCE(r.amount_q2,0) AS rq2, + COALESCE(r.amount_q3,0) AS rq3, COALESCE(r.amount_q4,0) AS rq4, + r.justification AS rjust, + cs.*, al.internal_order AS al_io, al.property_object AS al_po, + ca.approved_amount AS ca_amt, ca.protocol_reference AS ca_prot, ca.note AS ca_note, + ck.ceiling_amount AS ckk_ceil, + COALESCE(ck.expenses_q1,0) AS ckk1, COALESCE(ck.expenses_q2,0) AS ckk2, + COALESCE(ck.expenses_q3,0) AS ckk3, COALESCE(ck.expenses_q4,0) AS ckk4, + ck.rf_schedule AS ckk_rf, ck.delivery_deadline AS ckk_dl, + ck.procurement_plan AS ckk_pp, ck.procurement_method AS ckk_pm, ck.comment AS ckk_cmt, + cd.counterparty AS cd_cp, cd.reference AS cd_ref, cd.addenda AS cd_add, + cd.subject AS cd_subj, cd.currency AS cd_cur, cd.ceiling_amount AS cd_ceil, + COALESCE(cd.expenses_q1,0) AS cdq1, COALESCE(cd.expenses_q2,0) AS cdq2, + COALESCE(cd.expenses_q3,0) AS cdq3, COALESCE(cd.expenses_q4,0) AS cdq4, + cd.rf_schedule AS cd_rf, cd.vat_rate AS cd_vat, cd.exchange_rate AS cd_rate, + cd.amount_foreign AS cd_fx, cd.deadline AS cd_dl, cd.payment_scheme AS cd_sch, + cd.act AS cd_act, cd.comment AS cd_cmt2, + q1.adj_current AS q1_ac, q1.adj_ssp AS q1_as, q1.adj_rf AS q1_ar, q1.adj_reserve AS q1_arv, + q1.adj_comment AS q1_acmt, q1.payment_date AS q1_pd, q1.payment_amount AS q1_pa, + q1.payment_amount_ho AS q1_pho, q1.payment_amount_rf AS q1_prf, + q1.payment_comment AS q1_pcmt, q1.payment_act AS q1_pact, + q1.actual_m1 AS q1_m1, q1.actual_m2 AS q1_m2, q1.actual_m3 AS q1_m3, + q1.transfer_to_q2 AS q1_tq2, q1.transfer_to_q3 AS q1_tq3, q1.transfer_to_q4 AS q1_tq4, + q2.plan_revision_eco_change AS q2_reco, q2.plan_revision_item_adj AS q2_ritm, + q2.plan_revision_increase AS q2_rinc, q2.plan_revision_sequester AS q2_rseq, + q2.plan_revision_comment AS q2_rcmt, + q2.adj_current AS q2_ac, q2.adj_ssp AS q2_as, q2.adj_rf AS q2_ar, q2.adj_reserve AS q2_arv, + q2.adj_comment AS q2_acmt, q2.payment_date AS q2_pd, q2.payment_amount AS q2_pa, + q2.payment_amount_ho AS q2_pho, q2.payment_amount_rf AS q2_prf, + q2.payment_comment AS q2_pcmt, q2.payment_act AS q2_pact, + q2.actual_m1 AS q2_m1, q2.actual_m2 AS q2_m2, q2.actual_m3 AS q2_m3, + q2.transfer_to_q3 AS q2_tq3, q2.transfer_to_q4 AS q2_tq4, + q3.plan_revision_eco_change AS q3_reco, q3.plan_revision_item_adj AS q3_ritm, + q3.plan_revision_increase AS q3_rinc, q3.plan_revision_sequester AS q3_rseq, + q3.plan_revision_comment AS q3_rcmt, + q3.adj_current AS q3_ac, q3.adj_ssp AS q3_as, q3.adj_rf AS q3_ar, q3.adj_reserve AS q3_arv, + q3.adj_comment AS q3_acmt, q3.payment_date AS q3_pd, q3.payment_amount AS q3_pa, + q3.payment_amount_ho AS q3_pho, q3.payment_amount_rf AS q3_prf, + q3.payment_comment AS q3_pcmt, q3.payment_act AS q3_pact, + q3.actual_m1 AS q3_m1, q3.actual_m2 AS q3_m2, q3.actual_m3 AS q3_m3, + q3.transfer_to_q4 AS q3_tq4, + q4.plan_revision_eco_change AS q4_reco, q4.plan_revision_item_adj AS q4_ritm, + q4.plan_revision_increase AS q4_rinc, q4.plan_revision_sequester AS q4_rseq, + q4.plan_revision_comment AS q4_rcmt, + q4.adj_current AS q4_ac, q4.adj_ssp AS q4_as, q4.adj_rf AS q4_ar, q4.adj_reserve AS q4_arv, + q4.adj_comment AS q4_acmt, q4.payment_date AS q4_pd, q4.payment_amount AS q4_pa, + q4.payment_amount_ho AS q4_pho, q4.payment_amount_rf AS q4_prf, + q4.payment_comment AS q4_pcmt, q4.payment_act AS q4_pact, + q4.actual_m1 AS q4_m1, q4.actual_m2 AS q4_m2, q4.actual_m3 AS q4_m3, + q4.actual_spod AS q4_spod, + tot.tot_aq1, tot.tot_aq2, tot.tot_aq3, tot.tot_aq4, + tot.tot_pa1, tot.tot_pa2, tot.tot_pa3, tot.tot_pa4, + CASE WHEN COALESCE(cd.expenses_q1,0)>0 THEN cd.expenses_q1 ELSE ck.expenses_q1 END AS bk1, + CASE WHEN COALESCE(cd.expenses_q2,0)>0 THEN cd.expenses_q2 ELSE ck.expenses_q2 END AS bk2, + CASE WHEN COALESCE(cd.expenses_q3,0)>0 THEN cd.expenses_q3 ELSE ck.expenses_q3 END AS bk3, + CASE WHEN COALESCE(cd.expenses_q4,0)>0 THEN cd.expenses_q4 ELSE ck.expenses_q4 END AS bk4, + COALESCE(p.plan_q1,0)+COALESCE(sd.adj_q1,0)+COALESCE(r.amount_q1,0) AS ap1, + COALESCE(p.plan_q2,0)+COALESCE(sd.adj_q2,0)+COALESCE(r.amount_q2,0) AS ap2, + COALESCE(p.plan_q3,0)+COALESCE(sd.adj_q3,0)+COALESCE(r.amount_q3,0) AS ap3, + COALESCE(p.plan_q4,0)+COALESCE(sd.adj_q4,0)+COALESCE(r.amount_q4,0) AS ap4, + t.path AS tree_path + FROM page_f pg + JOIN tw t ON t.id = pg.eid + LEFT JOIN jp p ON p.line_id = pg.lid + LEFT JOIN jcs cs ON cs.line_id = pg.lid + LEFT JOIN jal al ON al.line_id = pg.lid + LEFT JOIN jsd sd ON sd.line_id = pg.lid + LEFT JOIN jr r ON r.line_id = pg.lid + LEFT JOIN jca ca ON ca.line_id = pg.lid + LEFT JOIN jck ck ON ck.line_id = pg.lid + LEFT JOIN jcd cd ON cd.line_id = pg.lid + LEFT JOIN jq1 q1 ON q1.line_id = pg.lid + LEFT JOIN jq2 q2 ON q2.line_id = pg.lid + LEFT JOIN jq3 q3 ON q3.line_id = pg.lid + LEFT JOIN jq4 q4 ON q4.line_id = pg.lid + LEFT JOIN jtot tot ON tot.tlid = pg.lid + ), + enriched AS ( + SELECT b.*, + b.ap1 + COALESCE(b.q1_ac,0)+COALESCE(b.q1_as,0)+COALESCE(b.q1_ar,0)+COALESCE(b.q1_arv,0) AS cp1, + COALESCE(b.q1_m1,0)+COALESCE(b.q1_m2,0)+COALESCE(b.q1_m3,0) AS aq1, + b.ap2 + COALESCE(b.q1_tq2,0) + COALESCE(b.q2_reco,0)+COALESCE(b.q2_ritm,0)+COALESCE(b.q2_rinc,0)+COALESCE(b.q2_rseq,0) AS np2, + COALESCE(b.q2_m1,0)+COALESCE(b.q2_m2,0)+COALESCE(b.q2_m3,0) AS aq2, + b.ap3 + COALESCE(b.q1_tq3,0)+COALESCE(b.q2_tq3,0) + COALESCE(b.q3_reco,0)+COALESCE(b.q3_ritm,0)+COALESCE(b.q3_rinc,0)+COALESCE(b.q3_rseq,0) AS np3, + COALESCE(b.q3_m1,0)+COALESCE(b.q3_m2,0)+COALESCE(b.q3_m3,0) AS aq3, + b.ap4 + COALESCE(b.q1_tq4,0)+COALESCE(b.q2_tq4,0)+COALESCE(b.q3_tq4,0) + COALESCE(b.q4_reco,0)+COALESCE(b.q4_ritm,0)+COALESCE(b.q4_rinc,0)+COALESCE(b.q4_rseq,0) AS np4, + COALESCE(b.q4_m1,0)+COALESCE(b.q4_m2,0)+COALESCE(b.q4_m3,0) AS aq4_no_spod, + COALESCE(b.q4_m1,0)+COALESCE(b.q4_m2,0)+COALESCE(b.q4_m3,0)+COALESCE(b.q4_spod,0) AS aq4 + FROM input_base b + ), + final_input AS ( + SELECT e.*, + e.np2 + COALESCE(e.q2_ac,0)+COALESCE(e.q2_as,0)+COALESCE(e.q2_ar,0)+COALESCE(e.q2_arv,0) AS cp2, + e.np3 + COALESCE(e.q3_ac,0)+COALESCE(e.q3_as,0)+COALESCE(e.q3_ar,0)+COALESCE(e.q3_arv,0) AS cp3, + e.np4 + COALESCE(e.q4_ac,0)+COALESCE(e.q4_as,0)+COALESCE(e.q4_ar,0)+COALESCE(e.q4_arv,0) AS cp4 + FROM enriched e + ), + + agg AS ( + SELECT bl.expense_item_id AS eid, + SUM(p.plan_q1) AS sp1, SUM(p.plan_q2) AS sp2, SUM(p.plan_q3) AS sp3, SUM(p.plan_q4) AS sp4, + SUM(sd.adj_q1) AS ss1, SUM(sd.adj_q2) AS ss2, SUM(sd.adj_q3) AS ss3, SUM(sd.adj_q4) AS ss4, + SUM(r.amount_q1) AS sr1, SUM(r.amount_q2) AS sr2, SUM(r.amount_q3) AS sr3, SUM(r.amount_q4) AS sr4, + SUM(ca.approved_amount) AS sca, + SUM(ck.ceiling_amount) AS sckc, + SUM(ck.expenses_q1) AS sck1, SUM(ck.expenses_q2) AS sck2, SUM(ck.expenses_q3) AS sck3, SUM(ck.expenses_q4) AS sck4, + SUM(cd.ceiling_amount) AS scdc, + SUM(cd.expenses_q1) AS scd1, SUM(cd.expenses_q2) AS scd2, SUM(cd.expenses_q3) AS scd3, SUM(cd.expenses_q4) AS scd4, + SUM(cs.total_amount) AS scst, SUM(cs.future_payments_y1) AS sfy1, SUM(cs.future_payments_y2) AS sfy2, + SUM(cs.other_ssp_amount) AS sossp + FROM budget_line bl + JOIN expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN plan p ON p.line_id = bl.id AND s_need_ap + LEFT JOIN contract_summary cs ON cs.line_id = bl.id AND s_cs + LEFT JOIN sequestration sd ON sd.line_id = bl.id AND sd.actor = 'DFIP' AND s_need_ap + LEFT JOIN reserve r ON r.line_id = bl.id AND s_need_ap + LEFT JOIN collegial_approval ca ON ca.line_id = bl.id AND s_ca + LEFT JOIN ckk ck ON ck.line_id = bl.id AND (s_ckk OR s_need_bk) + LEFT JOIN contract_detail cd ON cd.line_id = bl.id AND (s_cd OR s_need_bk) + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND (p_direction is null OR bl.direction is null or bl.direction = p_direction) + AND (p_focus_lines IS NULL OR bl.expense_item_id = ANY(v_focus_eids)) + AND (s_need_ap OR s_cs OR s_ca OR s_ckk OR s_cd OR s_need_bk) + GROUP BY bl.expense_item_id + ), + aq1 AS ( + SELECT bl.expense_item_id AS eid, + SUM(blq.adj_current) AS ac, SUM(blq.adj_ssp) AS as2, SUM(blq.adj_rf) AS ar, SUM(blq.adj_reserve) AS arv, + SUM(blq.payment_amount) AS pa, SUM(blq.payment_amount_ho) AS pho, SUM(blq.payment_amount_rf) AS prf, + SUM(blq.actual_m1) AS m1, SUM(blq.actual_m2) AS m2, SUM(blq.actual_m3) AS m3, + SUM(blq.transfer_to_q2) AS tq2, SUM(blq.transfer_to_q3) AS tq3, SUM(blq.transfer_to_q4) AS tq4 + FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 1 + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_q1 AND (p_direction is null OR bl.direction is null or bl.direction = p_direction) + AND (p_focus_lines IS NULL OR bl.expense_item_id = ANY(v_focus_eids)) + GROUP BY bl.expense_item_id + ), + aq2 AS ( + SELECT bl.expense_item_id AS eid, + SUM(blq.plan_revision_eco_change) AS reco, SUM(blq.plan_revision_item_adj) AS ritm, + SUM(blq.plan_revision_increase) AS rinc, SUM(blq.plan_revision_sequester) AS rseq, + SUM(blq.adj_current) AS ac, SUM(blq.adj_ssp) AS as2, SUM(blq.adj_rf) AS ar, SUM(blq.adj_reserve) AS arv, + SUM(blq.payment_amount) AS pa, SUM(blq.payment_amount_ho) AS pho, SUM(blq.payment_amount_rf) AS prf, + SUM(blq.actual_m1) AS m1, SUM(blq.actual_m2) AS m2, SUM(blq.actual_m3) AS m3, + SUM(blq.transfer_to_q3) AS tq3, SUM(blq.transfer_to_q4) AS tq4 + FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 2 + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_q2 AND (p_direction is null OR bl.direction is null or bl.direction = p_direction) + AND (p_focus_lines IS NULL OR bl.expense_item_id = ANY(v_focus_eids)) + GROUP BY bl.expense_item_id + ), + aq3 AS ( + SELECT bl.expense_item_id AS eid, + SUM(blq.plan_revision_eco_change) AS reco, SUM(blq.plan_revision_item_adj) AS ritm, + SUM(blq.plan_revision_increase) AS rinc, SUM(blq.plan_revision_sequester) AS rseq, + SUM(blq.adj_current) AS ac, SUM(blq.adj_ssp) AS as2, SUM(blq.adj_rf) AS ar, SUM(blq.adj_reserve) AS arv, + SUM(blq.payment_amount) AS pa, SUM(blq.payment_amount_ho) AS pho, SUM(blq.payment_amount_rf) AS prf, + SUM(blq.actual_m1) AS m1, SUM(blq.actual_m2) AS m2, SUM(blq.actual_m3) AS m3, + SUM(blq.transfer_to_q4) AS tq4 + FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 3 + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_q3 AND (p_direction is null OR bl.direction is null or bl.direction = p_direction) + AND (p_focus_lines IS NULL OR bl.expense_item_id = ANY(v_focus_eids)) + GROUP BY bl.expense_item_id + ), + aq4 AS ( + SELECT bl.expense_item_id AS eid, + SUM(blq.plan_revision_eco_change) AS reco, SUM(blq.plan_revision_item_adj) AS ritm, + SUM(blq.plan_revision_increase) AS rinc, SUM(blq.plan_revision_sequester) AS rseq, + SUM(blq.adj_current) AS ac, SUM(blq.adj_ssp) AS as2, SUM(blq.adj_rf) AS ar, SUM(blq.adj_reserve) AS arv, + SUM(blq.payment_amount) AS pa, SUM(blq.payment_amount_ho) AS pho, SUM(blq.payment_amount_rf) AS prf, + SUM(blq.actual_m1) AS m1, SUM(blq.actual_m2) AS m2, SUM(blq.actual_m3) AS m3, + SUM(blq.actual_spod) AS spod + FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 4 + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND (s_q4 OR s_tot) AND (p_direction is null OR bl.direction is null or bl.direction = p_direction) + AND (p_focus_lines IS NULL OR bl.expense_item_id = ANY(v_focus_eids)) + GROUP BY bl.expense_item_id + ), + atot AS ( + SELECT bl.expense_item_id AS eid, + SUM(CASE WHEN blq.quarter=1 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS fq1, + SUM(CASE WHEN blq.quarter=2 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS fq2, + SUM(CASE WHEN blq.quarter=3 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0) END) AS fq3, + SUM(CASE WHEN blq.quarter=4 THEN COALESCE(blq.actual_m1,0)+COALESCE(blq.actual_m2,0)+COALESCE(blq.actual_m3,0)+COALESCE(blq.actual_spod,0) END) AS fq4, + SUM(blq.payment_amount) AS pay + FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id + LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id + WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_tot AND (p_direction is null OR bl.direction is null or bl.direction = p_direction) + AND (p_focus_lines IS NULL OR bl.expense_item_id = ANY(v_focus_eids)) + GROUP BY bl.expense_item_id + ), + tw_agg AS ( + SELECT tw.id, + SUM(a.sp1) AS sp1, SUM(a.sp2) AS sp2, SUM(a.sp3) AS sp3, SUM(a.sp4) AS sp4, + SUM(a.ss1) AS ss1, SUM(a.ss2) AS ss2, SUM(a.ss3) AS ss3, SUM(a.ss4) AS ss4, + SUM(a.sr1) AS sr1, SUM(a.sr2) AS sr2, SUM(a.sr3) AS sr3, SUM(a.sr4) AS sr4, + SUM(a.sca) AS sca, + SUM(a.sckc) AS sckc, SUM(a.sck1) AS sck1, SUM(a.sck2) AS sck2, SUM(a.sck3) AS sck3, SUM(a.sck4) AS sck4, + SUM(a.scdc) AS scdc, SUM(a.scd1) AS scd1, SUM(a.scd2) AS scd2, SUM(a.scd3) AS scd3, SUM(a.scd4) AS scd4, + SUM(a.scst) AS scst, SUM(a.sfy1) AS sfy1, SUM(a.sfy2) AS sfy2, SUM(a.sossp) AS sossp, + SUM(CASE WHEN COALESCE(a.scd1,0)>0 THEN a.scd1 ELSE a.sck1 END) AS bk1, + SUM(CASE WHEN COALESCE(a.scd2,0)>0 THEN a.scd2 ELSE a.sck2 END) AS bk2, + SUM(CASE WHEN COALESCE(a.scd3,0)>0 THEN a.scd3 ELSE a.sck3 END) AS bk3, + SUM(CASE WHEN COALESCE(a.scd4,0)>0 THEN a.scd4 ELSE a.sck4 END) AS bk4 + FROM tw_f tw LEFT JOIN agg a ON a.eid = ANY(tw.desc_ids) + GROUP BY tw.id + ), + tw_aq1 AS ( + SELECT tw.id, SUM(b.ac) AS ac, SUM(b.as2) AS as2, SUM(b.ar) AS ar, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.pho) AS pho, SUM(b.prf) AS prf, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, + SUM(b.tq2) AS tq2, SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4 + FROM tw_f tw LEFT JOIN aq1 b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id + ), + tw_aq2 AS ( + SELECT tw.id, SUM(b.reco) AS reco, SUM(b.ritm) AS ritm, SUM(b.rinc) AS rinc, SUM(b.rseq) AS rseq, + SUM(b.ac) AS ac, SUM(b.as2) AS as2, SUM(b.ar) AS ar, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.pho) AS pho, SUM(b.prf) AS prf, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, + SUM(b.tq3) AS tq3, SUM(b.tq4) AS tq4 + FROM tw_f tw LEFT JOIN aq2 b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id + ), + tw_aq3 AS ( + SELECT tw.id, SUM(b.reco) AS reco, SUM(b.ritm) AS ritm, SUM(b.rinc) AS rinc, SUM(b.rseq) AS rseq, + SUM(b.ac) AS ac, SUM(b.as2) AS as2, SUM(b.ar) AS ar, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.pho) AS pho, SUM(b.prf) AS prf, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, + SUM(b.tq4) AS tq4 + FROM tw_f tw LEFT JOIN aq3 b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id + ), + tw_aq4 AS ( + SELECT tw.id, SUM(b.reco) AS reco, SUM(b.ritm) AS ritm, SUM(b.rinc) AS rinc, SUM(b.rseq) AS rseq, + SUM(b.ac) AS ac, SUM(b.as2) AS as2, SUM(b.ar) AS ar, SUM(b.arv) AS arv, + SUM(b.pa) AS pa, SUM(b.pho) AS pho, SUM(b.prf) AS prf, + SUM(b.m1) AS m1, SUM(b.m2) AS m2, SUM(b.m3) AS m3, + SUM(b.spod) AS spod + FROM tw_f tw LEFT JOIN aq4 b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id + ), + tw_atot AS ( + SELECT tw.id, SUM(b.fq1) AS fq1, SUM(b.fq2) AS fq2, SUM(b.fq3) AS fq3, SUM(b.fq4) AS fq4, + SUM(b.pay) AS pay + FROM tw_f tw LEFT JOIN atot b ON b.eid = ANY(tw.desc_ids) GROUP BY tw.id + ) + + SELECT sub.row_type, sub.depth, sub.line_id, sub.col_b_year, sub.col_c_section, sub.col_d_item_id, sub.col_e_num_group, sub.col_f_name, sub.col_g_internal_order, sub.col_h_plan_q1, sub.col_i_plan_q2, sub.col_j_plan_q3, sub.col_k_plan_q4, sub.col_l_plan_year, sub.col_m_plan_comment, sub.col_n_cs_total, sub.col_o_cs_counterparty, sub.col_p_cs_deadline, sub.col_q_cs_comment, sub.col_r_cs_future_y1, sub.col_s_cs_future_y2, sub.col_t_cs_other_ssp, sub.col_v_al_order, sub.col_w_al_property, sub.col_y_seq_q1, sub.col_z_seq_q2, sub.col_aa_seq_q3, sub.col_ab_seq_q4, sub.col_ac_seq_year, sub.col_ad_seq_justification, sub.col_af_res_q1, sub.col_ag_res_q2, sub.col_ah_res_q3, sub.col_ai_res_q4, sub.col_aj_res_year, sub.col_ak_res_justification, sub.col_am_approved_q1, sub.col_an_approved_q2, sub.col_ao_approved_q3, sub.col_ap_approved_q4, sub.col_aq_approved_year, sub.col_as_ca_approved, sub.col_at_ca_protocol, sub.col_au_ca_note, sub.col_aw_ckk_ceiling, sub.col_ax_ckk_exp_q1, sub.col_ay_ckk_exp_q2, sub.col_az_ckk_exp_q3, sub.col_ba_ckk_exp_q4, sub.col_bb_ckk_rf_sched, sub.col_bc_ckk_deadline, sub.col_bd_ckk_proc_plan, sub.col_be_ckk_proc_meth, sub.col_bf_ckk_comment, sub.col_bh_cd_counterparty, sub.col_bi_cd_reference, sub.col_bj_cd_addenda, sub.col_bk_cd_subject, sub.col_bl_cd_currency, sub.col_bm_cd_ceiling, sub.col_bn_cd_exp_q1, sub.col_bo_cd_exp_q2, sub.col_bp_cd_exp_q3, sub.col_bq_cd_exp_q4, sub.col_br_cd_rf_sched, sub.col_bs_cd_vat, sub.col_bt_cd_rate, sub.col_bu_cd_amount_fx, sub.col_bv_cd_deadline, sub.col_bw_cd_scheme, sub.col_bx_cd_act, sub.col_by_cd_comment, sub.col_ca_q1_adj_current, sub.col_cb_q1_adj_ssp, sub.col_cc_q1_adj_rf, sub.col_cd_q1_adj_reserve, sub.col_ce_q1_adj_comment, sub.col_cf_q1_corrected_plan, sub.col_ch_q1_pay_date, sub.col_ci_q1_pay_amount, sub.col_cj_q1_pay_ho, sub.col_ck_q1_pay_rf, sub.col_cl_q1_pay_comment, sub.col_cm_q1_pay_act, sub.col_co_q1_booking, sub.col_cp_q1_actual_m1, sub.col_cq_q1_actual_m2, sub.col_cr_q1_actual_m3, sub.col_cs_q1_actual_quarter, sub.col_ct_q1_rem_booking, sub.col_cu_q1_rem_actual, sub.col_cw_q1_trans_q2, sub.col_cx_q1_trans_q3, sub.col_cy_q1_trans_q4, sub.col_cz_q1_economy, sub.col_da_q1_total, sub.col_dc_q2_rev_eco, sub.col_dd_q2_rev_item, sub.col_de_q2_rev_inc, sub.col_df_q2_rev_seq, sub.col_dg_q2_rev_comment, sub.col_dh_q2_new_plan, sub.col_dj_q2_adj_current, sub.col_dk_q2_adj_ssp, sub.col_dl_q2_adj_rf, sub.col_dm_q2_adj_reserve, sub.col_dn_q2_adj_comment, sub.col_do_q2_corrected_plan, sub.col_dq_q2_pay_date, sub.col_dr_q2_pay_amount, sub.col_ds_q2_pay_ho, sub.col_dt_q2_pay_rf, sub.col_du_q2_pay_comment, sub.col_dv_q2_pay_act, sub.col_dx_q2_booking, sub.col_dy_q2_actual_m1, sub.col_dz_q2_actual_m2, sub.col_ea_q2_actual_m3, sub.col_eb_q2_actual_quarter, sub.col_ec_q2_rem_booking, sub.col_ed_q2_rem_actual, sub.col_ef_q2_trans_q3, sub.col_eg_q2_trans_q4, sub.col_eh_q2_economy, sub.col_ei_q2_total, sub.col_ek_q3_rev_eco, sub.col_el_q3_rev_item, sub.col_em_q3_rev_inc, sub.col_en_q3_rev_seq, sub.col_eo_q3_rev_comment, sub.col_ep_q3_new_plan, sub.col_er_q3_adj_current, sub.col_es_q3_adj_ssp, sub.col_et_q3_adj_rf, sub.col_eu_q3_adj_reserve, sub.col_ev_q3_adj_comment, sub.col_ew_q3_corrected_plan, sub.col_ey_q3_pay_date, sub.col_ez_q3_pay_amount, sub.col_fa_q3_pay_ho, sub.col_fb_q3_pay_rf, sub.col_fc_q3_pay_comment, sub.col_fd_q3_pay_act, sub.col_ff_q3_booking, sub.col_fg_q3_actual_m1, sub.col_fh_q3_actual_m2, sub.col_fi_q3_actual_m3, sub.col_fj_q3_actual_quarter, sub.col_fk_q3_rem_booking, sub.col_fl_q3_rem_actual, sub.col_fn_q3_trans_q4, sub.col_fo_q3_economy, sub.col_fp_q3_total, sub.col_fr_q4_rev_eco, sub.col_fs_q4_rev_item, sub.col_ft_q4_rev_inc, sub.col_fu_q4_rev_seq, sub.col_fv_q4_rev_comment, sub.col_fw_q4_new_plan, sub.col_fy_q4_adj_current, sub.col_fz_q4_adj_ssp, sub.col_ga_q4_adj_rf, sub.col_gb_q4_adj_reserve, sub.col_gc_q4_adj_comment, sub.col_gd_q4_corrected_plan, sub.col_gf_q4_pay_date, sub.col_gg_q4_pay_amount, sub.col_gh_q4_pay_ho, sub.col_gi_q4_pay_rf, sub.col_gj_q4_pay_comment, sub.col_gk_q4_pay_act, sub.col_gm_q4_booking, sub.col_gn_q4_actual_m1, sub.col_go_q4_actual_m2, sub.col_gp_q4_actual_m3, sub.col_gq_q4_actual_quarter, sub.col_gr_q4_actual_spod, sub.col_gs_q4_rem_booking, sub.col_gt_q4_rem_actual, sub.col_gv_q4_economy, sub.col_gw_q4_total, sub.col_gz_fact_year, sub.col_hb_pay_year, sub._sort_path, sk.rnk + FROM ( + SELECT + 'INPUT'::VARCHAR, array_length(f.tree_path, 1)::INT, f.lid, + v_year, f.sc, f.ic, f.ng, COALESCE(f.bname, f.ename), f.io, + CASE WHEN s_plan THEN f.pq1 END, CASE WHEN s_plan THEN f.pq2 END, + CASE WHEN s_plan THEN f.pq3 END, CASE WHEN s_plan THEN f.pq4 END, + CASE WHEN s_plan THEN f.pq1+f.pq2+f.pq3+f.pq4 END, CASE WHEN s_plan THEN f.pcmt END, + CASE WHEN s_cs THEN f.total_amount END, CASE WHEN s_cs THEN f.counterparty END, + CASE WHEN s_cs THEN f.deadline END, CASE WHEN s_cs THEN f.comment END, + CASE WHEN s_cs THEN f.future_payments_y1 END, CASE WHEN s_cs THEN f.future_payments_y2 END, + CASE WHEN s_cs THEN f.other_ssp_amount END, + CASE WHEN s_al THEN f.al_io END, CASE WHEN s_al THEN f.al_po END, + CASE WHEN s_seq THEN f.sq1 END, CASE WHEN s_seq THEN f.sq2 END, + CASE WHEN s_seq THEN f.sq3 END, CASE WHEN s_seq THEN f.sq4 END, + CASE WHEN s_seq THEN f.sq1+f.sq2+f.sq3+f.sq4 END, CASE WHEN s_seq THEN f.sjust END, + CASE WHEN s_res THEN f.rq1 END, CASE WHEN s_res THEN f.rq2 END, + CASE WHEN s_res THEN f.rq3 END, CASE WHEN s_res THEN f.rq4 END, + CASE WHEN s_res THEN f.rq1+f.rq2+f.rq3+f.rq4 END, CASE WHEN s_res THEN f.rjust END, + CASE WHEN s_appr THEN f.ap1 END, CASE WHEN s_appr THEN f.ap2 END, + CASE WHEN s_appr THEN f.ap3 END, CASE WHEN s_appr THEN f.ap4 END, + CASE WHEN s_appr THEN f.ap1+f.ap2+f.ap3+f.ap4 END, + CASE WHEN s_ca THEN f.ca_amt END, CASE WHEN s_ca THEN f.ca_prot END, CASE WHEN s_ca THEN f.ca_note END, + CASE WHEN s_ckk THEN f.ckk_ceil END, CASE WHEN s_ckk THEN f.ckk1 END, CASE WHEN s_ckk THEN f.ckk2 END, + CASE WHEN s_ckk THEN f.ckk3 END, CASE WHEN s_ckk THEN f.ckk4 END, + CASE WHEN s_ckk THEN f.ckk_rf END, CASE WHEN s_ckk THEN f.ckk_dl END, + CASE WHEN s_ckk THEN f.ckk_pp END, CASE WHEN s_ckk THEN f.ckk_pm END, CASE WHEN s_ckk THEN f.ckk_cmt END, + CASE WHEN s_cd THEN f.cd_cp END, CASE WHEN s_cd THEN f.cd_ref END, CASE WHEN s_cd THEN f.cd_add END, + CASE WHEN s_cd THEN f.cd_subj END, CASE WHEN s_cd THEN f.cd_cur END, CASE WHEN s_cd THEN f.cd_ceil END, + CASE WHEN s_cd THEN f.cdq1 END, CASE WHEN s_cd THEN f.cdq2 END, + CASE WHEN s_cd THEN f.cdq3 END, CASE WHEN s_cd THEN f.cdq4 END, + CASE WHEN s_cd THEN f.cd_rf END, CASE WHEN s_cd THEN f.cd_vat END, + CASE WHEN s_cd THEN f.cd_rate END, CASE WHEN s_cd THEN f.cd_fx END, + CASE WHEN s_cd THEN f.cd_dl END, CASE WHEN s_cd THEN f.cd_sch END, + CASE WHEN s_cd THEN f.cd_act END, CASE WHEN s_cd THEN f.cd_cmt2 END, + CASE WHEN s_q1 THEN f.q1_ac END, CASE WHEN s_q1 THEN f.q1_as END, + CASE WHEN s_q1 THEN f.q1_ar END, CASE WHEN s_q1 THEN f.q1_arv END, + CASE WHEN s_q1 THEN f.q1_acmt END, CASE WHEN s_q1 THEN f.cp1 END, + CASE WHEN s_q1 THEN f.q1_pd END, CASE WHEN s_q1 THEN f.q1_pa END, + CASE WHEN s_q1 THEN f.q1_pho END, CASE WHEN s_q1 THEN f.q1_prf END, + CASE WHEN s_q1 THEN f.q1_pcmt END, CASE WHEN s_q1 THEN f.q1_pact END, + CASE WHEN s_q1 THEN COALESCE(f.bk1,0) END, + CASE WHEN s_q1 THEN f.q1_m1 END, CASE WHEN s_q1 THEN f.q1_m2 END, CASE WHEN s_q1 THEN f.q1_m3 END, + CASE WHEN s_q1 THEN f.aq1 END, + CASE WHEN s_q1 THEN f.cp1 - COALESCE(f.bk1,0) END, + CASE WHEN s_q1 THEN f.cp1 - f.aq1 END, + CASE WHEN s_q1 THEN f.q1_tq2 END, CASE WHEN s_q1 THEN f.q1_tq3 END, CASE WHEN s_q1 THEN f.q1_tq4 END, + CASE WHEN s_q1 THEN f.cp1 - f.aq1 - COALESCE(f.q1_tq2,0) - COALESCE(f.q1_tq3,0) - COALESCE(f.q1_tq4,0) END, + CASE WHEN s_q1 THEN f.cp1 - f.aq1 END, + CASE WHEN s_q2 THEN f.q2_reco END, CASE WHEN s_q2 THEN f.q2_ritm END, + CASE WHEN s_q2 THEN f.q2_rinc END, CASE WHEN s_q2 THEN f.q2_rseq END, + CASE WHEN s_q2 THEN f.q2_rcmt END, CASE WHEN s_q2 THEN f.np2 END, + CASE WHEN s_q2 THEN f.q2_ac END, CASE WHEN s_q2 THEN f.q2_as END, + CASE WHEN s_q2 THEN f.q2_ar END, CASE WHEN s_q2 THEN f.q2_arv END, + CASE WHEN s_q2 THEN f.q2_acmt END, CASE WHEN s_q2 THEN f.cp2 END, + CASE WHEN s_q2 THEN f.q2_pd END, CASE WHEN s_q2 THEN f.q2_pa END, + CASE WHEN s_q2 THEN f.q2_pho END, CASE WHEN s_q2 THEN f.q2_prf END, + CASE WHEN s_q2 THEN f.q2_pcmt END, CASE WHEN s_q2 THEN f.q2_pact END, + CASE WHEN s_q2 THEN COALESCE(f.bk2,0) END, + CASE WHEN s_q2 THEN f.q2_m1 END, CASE WHEN s_q2 THEN f.q2_m2 END, CASE WHEN s_q2 THEN f.q2_m3 END, + CASE WHEN s_q2 THEN f.aq2 END, + CASE WHEN s_q2 THEN f.cp2 - COALESCE(f.bk2,0) END, + CASE WHEN s_q2 THEN f.cp2 - f.aq2 END, + CASE WHEN s_q2 THEN f.q2_tq3 END, CASE WHEN s_q2 THEN f.q2_tq4 END, + CASE WHEN s_q2 THEN f.cp2 - f.aq2 - COALESCE(f.q2_tq3,0) - COALESCE(f.q2_tq4,0) END, + CASE WHEN s_q2 THEN f.cp2 - f.aq2 END, + CASE WHEN s_q3 THEN f.q3_reco END, CASE WHEN s_q3 THEN f.q3_ritm END, + CASE WHEN s_q3 THEN f.q3_rinc END, CASE WHEN s_q3 THEN f.q3_rseq END, + CASE WHEN s_q3 THEN f.q3_rcmt END, CASE WHEN s_q3 THEN f.np3 END, + CASE WHEN s_q3 THEN f.q3_ac END, CASE WHEN s_q3 THEN f.q3_as END, + CASE WHEN s_q3 THEN f.q3_ar END, CASE WHEN s_q3 THEN f.q3_arv END, + CASE WHEN s_q3 THEN f.q3_acmt END, CASE WHEN s_q3 THEN f.cp3 END, + CASE WHEN s_q3 THEN f.q3_pd END, CASE WHEN s_q3 THEN f.q3_pa END, + CASE WHEN s_q3 THEN f.q3_pho END, CASE WHEN s_q3 THEN f.q3_prf END, + CASE WHEN s_q3 THEN f.q3_pcmt END, CASE WHEN s_q3 THEN f.q3_pact END, + CASE WHEN s_q3 THEN COALESCE(f.bk3,0) END, + CASE WHEN s_q3 THEN f.q3_m1 END, CASE WHEN s_q3 THEN f.q3_m2 END, CASE WHEN s_q3 THEN f.q3_m3 END, + CASE WHEN s_q3 THEN f.aq3 END, + CASE WHEN s_q3 THEN f.cp3 - COALESCE(f.bk3,0) END, + CASE WHEN s_q3 THEN f.cp3 - f.aq3 END, + CASE WHEN s_q3 THEN f.q3_tq4 END, + CASE WHEN s_q3 THEN f.cp3 - f.aq3 - COALESCE(f.q3_tq4,0) END, + CASE WHEN s_q3 THEN f.cp3 - f.aq3 END, + CASE WHEN s_q4 THEN f.q4_reco END, CASE WHEN s_q4 THEN f.q4_ritm END, + CASE WHEN s_q4 THEN f.q4_rinc END, CASE WHEN s_q4 THEN f.q4_rseq END, + CASE WHEN s_q4 THEN f.q4_rcmt END, CASE WHEN s_q4 THEN f.np4 END, + CASE WHEN s_q4 THEN f.q4_ac END, CASE WHEN s_q4 THEN f.q4_as END, + CASE WHEN s_q4 THEN f.q4_ar END, CASE WHEN s_q4 THEN f.q4_arv END, + CASE WHEN s_q4 THEN f.q4_acmt END, CASE WHEN s_q4 THEN f.cp4 END, + CASE WHEN s_q4 THEN f.q4_pd END, CASE WHEN s_q4 THEN f.q4_pa END, + CASE WHEN s_q4 THEN f.q4_pho END, CASE WHEN s_q4 THEN f.q4_prf END, + CASE WHEN s_q4 THEN f.q4_pcmt END, CASE WHEN s_q4 THEN f.q4_pact END, + CASE WHEN s_q4 THEN COALESCE(f.bk4,0) END, + CASE WHEN s_q4 THEN f.q4_m1 END, CASE WHEN s_q4 THEN f.q4_m2 END, CASE WHEN s_q4 THEN f.q4_m3 END, + CASE WHEN s_q4 THEN f.aq4_no_spod END, + CASE WHEN s_q4 THEN f.q4_spod END, + CASE WHEN s_q4 THEN f.cp4 - COALESCE(f.bk4,0) END, + CASE WHEN s_q4 THEN f.cp4 - f.aq4 END, + CASE WHEN s_q4 THEN f.cp4 - f.aq4 END, + CASE WHEN s_q4 THEN f.cp4 - f.aq4 END, + CASE WHEN s_tot THEN COALESCE(f.tot_aq1,0)+COALESCE(f.tot_aq2,0)+COALESCE(f.tot_aq3,0)+COALESCE(f.tot_aq4,0) END, + CASE WHEN s_tot THEN COALESCE(f.tot_pa1,0)+COALESCE(f.tot_pa2,0)+COALESCE(f.tot_pa3,0)+COALESCE(f.tot_pa4,0) END, + f.tree_path || ARRAY[f.lid] AS _sort, + f.lid::BIGINT AS _k + FROM final_input f + + UNION ALL + + SELECT + CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP' WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' WHEN 4 THEN 'SUB_SUB_ITEM' END::VARCHAR, + t.depth, NULL::INT, + v_year, + CASE + WHEN t.depth < 3 THEN t.item_id + WHEN t.depth = 3 THEN t.parent_item_id + WHEN t.depth = 4 THEN t.grandparent_item_id + END, + CASE WHEN t.depth >= 3 THEN t.item_id ELSE NULL::VARCHAR END, + t.num_group_id, t.name, NULL::VARCHAR, + CASE WHEN s_plan THEN ta.sp1 END, + CASE WHEN s_plan THEN ta.sp2 END, + CASE WHEN s_plan THEN ta.sp3 END, + CASE WHEN s_plan THEN ta.sp4 END, + CASE WHEN s_plan THEN (COALESCE(ta.sp1,0)+COALESCE(ta.sp2,0)+COALESCE(ta.sp3,0)+COALESCE(ta.sp4,0)) END, + NULL::VARCHAR, + CASE WHEN s_cs THEN ta.scst END, + NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_cs THEN ta.sfy1 END, + CASE WHEN s_cs THEN ta.sfy2 END, + CASE WHEN s_cs THEN ta.sossp END, + NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_seq THEN ta.ss1 END, + CASE WHEN s_seq THEN ta.ss2 END, + CASE WHEN s_seq THEN ta.ss3 END, + CASE WHEN s_seq THEN ta.ss4 END, + CASE WHEN s_seq THEN (COALESCE(ta.ss1,0)+COALESCE(ta.ss2,0)+COALESCE(ta.ss3,0)+COALESCE(ta.ss4,0)) END, + NULL::VARCHAR, + CASE WHEN s_res THEN ta.sr1 END, + CASE WHEN s_res THEN ta.sr2 END, + CASE WHEN s_res THEN ta.sr3 END, + CASE WHEN s_res THEN ta.sr4 END, + CASE WHEN s_res THEN (COALESCE(ta.sr1,0)+COALESCE(ta.sr2,0)+COALESCE(ta.sr3,0)+COALESCE(ta.sr4,0)) END, + NULL::VARCHAR, + CASE WHEN s_appr THEN (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) END, + CASE WHEN s_appr THEN (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) END, + CASE WHEN s_appr THEN (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) END, + CASE WHEN s_appr THEN (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) END, + CASE WHEN s_appr THEN (COALESCE(ta.sp1,0)+COALESCE(ta.sp2,0)+COALESCE(ta.sp3,0)+COALESCE(ta.sp4,0) + +COALESCE(ta.ss1,0)+COALESCE(ta.ss2,0)+COALESCE(ta.ss3,0)+COALESCE(ta.ss4,0) + +COALESCE(ta.sr1,0)+COALESCE(ta.sr2,0)+COALESCE(ta.sr3,0)+COALESCE(ta.sr4,0)) END, + CASE WHEN s_ca THEN ta.sca END, + NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_ckk THEN ta.sckc END, + CASE WHEN s_ckk THEN ta.sck1 END, + CASE WHEN s_ckk THEN ta.sck2 END, + CASE WHEN s_ckk THEN ta.sck3 END, + CASE WHEN s_ckk THEN ta.sck4 END, + NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, + NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_cd THEN ta.scdc END, + CASE WHEN s_cd THEN ta.scd1 END, + CASE WHEN s_cd THEN ta.scd2 END, + CASE WHEN s_cd THEN ta.scd3 END, + CASE WHEN s_cd THEN ta.scd4 END, + NULL::VARCHAR, NULL::VARCHAR, NULL::NUMERIC, NULL::NUMERIC, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_q1 THEN tb1.ac END, + CASE WHEN s_q1 THEN tb1.as2 END, + CASE WHEN s_q1 THEN tb1.ar END, + CASE WHEN s_q1 THEN tb1.arv END, + NULL::VARCHAR, + CASE WHEN s_q1 THEN + (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) + + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)), 0) + END, + NULL::DATE, + CASE WHEN s_q1 THEN tb1.pa END, + CASE WHEN s_q1 THEN tb1.pho END, + CASE WHEN s_q1 THEN tb1.prf END, + NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_q1 THEN ta.bk1 END, + CASE WHEN s_q1 THEN tb1.m1 END, + CASE WHEN s_q1 THEN tb1.m2 END, + CASE WHEN s_q1 THEN tb1.m3 END, + CASE WHEN s_q1 THEN (COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)) END, + CASE WHEN s_q1 THEN + (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) + + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)),0) + - COALESCE(ta.bk1,0) + END, + CASE WHEN s_q1 THEN + (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) + + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)),0) + - COALESCE((COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)),0) + END, + CASE WHEN s_q1 THEN tb1.tq2 END, + CASE WHEN s_q1 THEN tb1.tq3 END, + CASE WHEN s_q1 THEN tb1.tq4 END, + CASE WHEN s_q1 THEN + (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) + + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)),0) + - COALESCE((COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)),0) + - COALESCE((COALESCE(tb1.tq2,0)+COALESCE(tb1.tq3,0)+COALESCE(tb1.tq4,0)),0) + END, + CASE WHEN s_q1 THEN + (COALESCE(ta.sp1,0)+COALESCE(ta.ss1,0)+COALESCE(ta.sr1,0)) + + COALESCE((COALESCE(tb1.ac,0)+COALESCE(tb1.as2,0)+COALESCE(tb1.ar,0)+COALESCE(tb1.arv,0)),0) + - COALESCE((COALESCE(tb1.m1,0)+COALESCE(tb1.m2,0)+COALESCE(tb1.m3,0)),0) + END, + CASE WHEN s_q2 THEN tb2.reco END, + CASE WHEN s_q2 THEN tb2.ritm END, + CASE WHEN s_q2 THEN tb2.rinc END, + CASE WHEN s_q2 THEN tb2.rseq END, + NULL::VARCHAR, + CASE WHEN s_q2 THEN + (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) + + COALESCE(tb1.tq2, 0) + + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)), 0) + END, + CASE WHEN s_q2 THEN tb2.ac END, + CASE WHEN s_q2 THEN tb2.as2 END, + CASE WHEN s_q2 THEN tb2.ar END, + CASE WHEN s_q2 THEN tb2.arv END, + NULL::VARCHAR, + CASE WHEN s_q2 THEN + (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) + + COALESCE(tb1.tq2, 0) + + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)), 0) + END, + NULL::DATE, + CASE WHEN s_q2 THEN tb2.pa END, + CASE WHEN s_q2 THEN tb2.pho END, + CASE WHEN s_q2 THEN tb2.prf END, + NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_q2 THEN ta.bk2 END, + CASE WHEN s_q2 THEN tb2.m1 END, + CASE WHEN s_q2 THEN tb2.m2 END, + CASE WHEN s_q2 THEN tb2.m3 END, + CASE WHEN s_q2 THEN (COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)) END, + CASE WHEN s_q2 THEN + (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) + + COALESCE(tb1.tq2,0) + + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)),0) + - COALESCE(ta.bk2,0) + END, + CASE WHEN s_q2 THEN + (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) + + COALESCE(tb1.tq2,0) + + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)),0) + - COALESCE((COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)),0) + END, + CASE WHEN s_q2 THEN tb2.tq3 END, + CASE WHEN s_q2 THEN tb2.tq4 END, + CASE WHEN s_q2 THEN + (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) + + COALESCE(tb1.tq2,0) + + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)),0) + - COALESCE((COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)),0) + - COALESCE((COALESCE(tb2.tq3,0)+COALESCE(tb2.tq4,0)),0) + END, + CASE WHEN s_q2 THEN + (COALESCE(ta.sp2,0)+COALESCE(ta.ss2,0)+COALESCE(ta.sr2,0)) + + COALESCE(tb1.tq2,0) + + COALESCE((COALESCE(tb2.reco,0)+COALESCE(tb2.ritm,0)+COALESCE(tb2.rinc,0)+COALESCE(tb2.rseq,0)+COALESCE(tb2.ac,0)+COALESCE(tb2.as2,0)+COALESCE(tb2.ar,0)+COALESCE(tb2.arv,0)),0) + - COALESCE((COALESCE(tb2.m1,0)+COALESCE(tb2.m2,0)+COALESCE(tb2.m3,0)),0) + END, + CASE WHEN s_q3 THEN tb3.reco END, + CASE WHEN s_q3 THEN tb3.ritm END, + CASE WHEN s_q3 THEN tb3.rinc END, + CASE WHEN s_q3 THEN tb3.rseq END, + NULL::VARCHAR, + CASE WHEN s_q3 THEN + (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) + + COALESCE(tb1.tq3, 0) + + COALESCE(tb2.tq3, 0) + + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)), 0) + END, + CASE WHEN s_q3 THEN tb3.ac END, + CASE WHEN s_q3 THEN tb3.as2 END, + CASE WHEN s_q3 THEN tb3.ar END, + CASE WHEN s_q3 THEN tb3.arv END, + NULL::VARCHAR, + CASE WHEN s_q3 THEN + (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) + + COALESCE(tb1.tq3, 0) + + COALESCE(tb2.tq3, 0) + + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)), 0) + END, + NULL::DATE, + CASE WHEN s_q3 THEN tb3.pa END, + CASE WHEN s_q3 THEN tb3.pho END, + CASE WHEN s_q3 THEN tb3.prf END, + NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_q3 THEN ta.bk3 END, + CASE WHEN s_q3 THEN tb3.m1 END, + CASE WHEN s_q3 THEN tb3.m2 END, + CASE WHEN s_q3 THEN tb3.m3 END, + CASE WHEN s_q3 THEN (COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)) END, + CASE WHEN s_q3 THEN + (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) + + COALESCE(tb1.tq3,0) + + COALESCE(tb2.tq3,0) + + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)),0) + - COALESCE(ta.bk3,0) + END, + CASE WHEN s_q3 THEN + (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) + + COALESCE(tb1.tq3,0) + + COALESCE(tb2.tq3,0) + + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)),0) + - COALESCE((COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)),0) + END, + CASE WHEN s_q3 THEN tb3.tq4 END, + CASE WHEN s_q3 THEN + (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) + + COALESCE(tb1.tq3,0) + + COALESCE(tb2.tq3,0) + + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)),0) + - COALESCE((COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)),0) + - COALESCE(COALESCE(tb3.tq4,0),0) + END, + CASE WHEN s_q3 THEN + (COALESCE(ta.sp3,0)+COALESCE(ta.ss3,0)+COALESCE(ta.sr3,0)) + + COALESCE(tb1.tq3,0) + + COALESCE(tb2.tq3,0) + + COALESCE((COALESCE(tb3.reco,0)+COALESCE(tb3.ritm,0)+COALESCE(tb3.rinc,0)+COALESCE(tb3.rseq,0)+COALESCE(tb3.ac,0)+COALESCE(tb3.as2,0)+COALESCE(tb3.ar,0)+COALESCE(tb3.arv,0)),0) + - COALESCE((COALESCE(tb3.m1,0)+COALESCE(tb3.m2,0)+COALESCE(tb3.m3,0)),0) + END, + CASE WHEN s_q4 THEN tb4.reco END, + CASE WHEN s_q4 THEN tb4.ritm END, + CASE WHEN s_q4 THEN tb4.rinc END, + CASE WHEN s_q4 THEN tb4.rseq END, + NULL::VARCHAR, + CASE WHEN s_q4 THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) + + COALESCE(tb1.tq4, 0) + + COALESCE(tb2.tq4, 0) + + COALESCE(tb3.tq4, 0) + + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)), 0) + END, + CASE WHEN s_q4 THEN tb4.ac END, + CASE WHEN s_q4 THEN tb4.as2 END, + CASE WHEN s_q4 THEN tb4.ar END, + CASE WHEN s_q4 THEN tb4.arv END, + NULL::VARCHAR, + CASE WHEN s_q4 THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) + + COALESCE(tb1.tq4, 0) + + COALESCE(tb2.tq4, 0) + + COALESCE(tb3.tq4, 0) + + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)), 0) + END, + NULL::DATE, + CASE WHEN s_q4 THEN tb4.pa END, + CASE WHEN s_q4 THEN tb4.pho END, + CASE WHEN s_q4 THEN tb4.prf END, + NULL::VARCHAR, NULL::VARCHAR, + CASE WHEN s_q4 THEN ta.bk4 END, + CASE WHEN s_q4 THEN tb4.m1 END, + CASE WHEN s_q4 THEN tb4.m2 END, + CASE WHEN s_q4 THEN tb4.m3 END, + CASE WHEN s_q4 THEN (COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)) END, + CASE WHEN s_q4 THEN tb4.spod END, + CASE WHEN s_q4 THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) + + COALESCE(tb1.tq4,0) + + COALESCE(tb2.tq4,0) + + COALESCE(tb3.tq4,0) + + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)),0) + - COALESCE(ta.bk4,0) + END, + CASE WHEN s_q4 THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) + + COALESCE(tb1.tq4,0) + + COALESCE(tb2.tq4,0) + + COALESCE(tb3.tq4,0) + + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)),0) + - COALESCE((COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)),0) + END, + CASE WHEN s_q4 THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) + + COALESCE(tb1.tq4,0) + + COALESCE(tb2.tq4,0) + + COALESCE(tb3.tq4,0) + + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)),0) + - COALESCE((COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)),0) + END, + CASE WHEN s_q4 THEN + (COALESCE(ta.sp4,0)+COALESCE(ta.ss4,0)+COALESCE(ta.sr4,0)) + + COALESCE(tb1.tq4,0) + + COALESCE(tb2.tq4,0) + + COALESCE(tb3.tq4,0) + + COALESCE((COALESCE(tb4.reco,0)+COALESCE(tb4.ritm,0)+COALESCE(tb4.rinc,0)+COALESCE(tb4.rseq,0)+COALESCE(tb4.ac,0)+COALESCE(tb4.as2,0)+COALESCE(tb4.ar,0)+COALESCE(tb4.arv,0)),0) + - COALESCE((COALESCE(tb4.m1,0)+COALESCE(tb4.m2,0)+COALESCE(tb4.m3,0)+COALESCE(tb4.spod,0)),0) + END, + CASE WHEN s_tot THEN (COALESCE(tt.fq1,0)+COALESCE(tt.fq2,0)+COALESCE(tt.fq3,0)+COALESCE(tt.fq4,0)) END, + CASE WHEN s_tot THEN tt.pay END, + t.path AS _sort, + -t.id::BIGINT + + FROM tw_f t + LEFT JOIN tw_agg ta ON ta.id = t.id + LEFT JOIN tw_aq1 tb1 ON tb1.id = t.id + LEFT JOIN tw_aq2 tb2 ON tb2.id = t.id + LEFT JOIN tw_aq3 tb3 ON tb3.id = t.id + LEFT JOIN tw_aq4 tb4 ON tb4.id = t.id + LEFT JOIN tw_atot tt ON tt.id = t.id + ) sub(row_type, depth, line_id, col_b_year, col_c_section, col_d_item_id, col_e_num_group, col_f_name, col_g_internal_order, col_h_plan_q1, col_i_plan_q2, col_j_plan_q3, col_k_plan_q4, col_l_plan_year, col_m_plan_comment, col_n_cs_total, col_o_cs_counterparty, col_p_cs_deadline, col_q_cs_comment, col_r_cs_future_y1, col_s_cs_future_y2, col_t_cs_other_ssp, col_v_al_order, col_w_al_property, col_y_seq_q1, col_z_seq_q2, col_aa_seq_q3, col_ab_seq_q4, col_ac_seq_year, col_ad_seq_justification, col_af_res_q1, col_ag_res_q2, col_ah_res_q3, col_ai_res_q4, col_aj_res_year, col_ak_res_justification, col_am_approved_q1, col_an_approved_q2, col_ao_approved_q3, col_ap_approved_q4, col_aq_approved_year, col_as_ca_approved, col_at_ca_protocol, col_au_ca_note, col_aw_ckk_ceiling, col_ax_ckk_exp_q1, col_ay_ckk_exp_q2, col_az_ckk_exp_q3, col_ba_ckk_exp_q4, col_bb_ckk_rf_sched, col_bc_ckk_deadline, col_bd_ckk_proc_plan, col_be_ckk_proc_meth, col_bf_ckk_comment, col_bh_cd_counterparty, col_bi_cd_reference, col_bj_cd_addenda, col_bk_cd_subject, col_bl_cd_currency, col_bm_cd_ceiling, col_bn_cd_exp_q1, col_bo_cd_exp_q2, col_bp_cd_exp_q3, col_bq_cd_exp_q4, col_br_cd_rf_sched, col_bs_cd_vat, col_bt_cd_rate, col_bu_cd_amount_fx, col_bv_cd_deadline, col_bw_cd_scheme, col_bx_cd_act, col_by_cd_comment, col_ca_q1_adj_current, col_cb_q1_adj_ssp, col_cc_q1_adj_rf, col_cd_q1_adj_reserve, col_ce_q1_adj_comment, col_cf_q1_corrected_plan, col_ch_q1_pay_date, col_ci_q1_pay_amount, col_cj_q1_pay_ho, col_ck_q1_pay_rf, col_cl_q1_pay_comment, col_cm_q1_pay_act, col_co_q1_booking, col_cp_q1_actual_m1, col_cq_q1_actual_m2, col_cr_q1_actual_m3, col_cs_q1_actual_quarter, col_ct_q1_rem_booking, col_cu_q1_rem_actual, col_cw_q1_trans_q2, col_cx_q1_trans_q3, col_cy_q1_trans_q4, col_cz_q1_economy, col_da_q1_total, col_dc_q2_rev_eco, col_dd_q2_rev_item, col_de_q2_rev_inc, col_df_q2_rev_seq, col_dg_q2_rev_comment, col_dh_q2_new_plan, col_dj_q2_adj_current, col_dk_q2_adj_ssp, col_dl_q2_adj_rf, col_dm_q2_adj_reserve, col_dn_q2_adj_comment, col_do_q2_corrected_plan, col_dq_q2_pay_date, col_dr_q2_pay_amount, col_ds_q2_pay_ho, col_dt_q2_pay_rf, col_du_q2_pay_comment, col_dv_q2_pay_act, col_dx_q2_booking, col_dy_q2_actual_m1, col_dz_q2_actual_m2, col_ea_q2_actual_m3, col_eb_q2_actual_quarter, col_ec_q2_rem_booking, col_ed_q2_rem_actual, col_ef_q2_trans_q3, col_eg_q2_trans_q4, col_eh_q2_economy, col_ei_q2_total, col_ek_q3_rev_eco, col_el_q3_rev_item, col_em_q3_rev_inc, col_en_q3_rev_seq, col_eo_q3_rev_comment, col_ep_q3_new_plan, col_er_q3_adj_current, col_es_q3_adj_ssp, col_et_q3_adj_rf, col_eu_q3_adj_reserve, col_ev_q3_adj_comment, col_ew_q3_corrected_plan, col_ey_q3_pay_date, col_ez_q3_pay_amount, col_fa_q3_pay_ho, col_fb_q3_pay_rf, col_fc_q3_pay_comment, col_fd_q3_pay_act, col_ff_q3_booking, col_fg_q3_actual_m1, col_fh_q3_actual_m2, col_fi_q3_actual_m3, col_fj_q3_actual_quarter, col_fk_q3_rem_booking, col_fl_q3_rem_actual, col_fn_q3_trans_q4, col_fo_q3_economy, col_fp_q3_total, col_fr_q4_rev_eco, col_fs_q4_rev_item, col_ft_q4_rev_inc, col_fu_q4_rev_seq, col_fv_q4_rev_comment, col_fw_q4_new_plan, col_fy_q4_adj_current, col_fz_q4_adj_ssp, col_ga_q4_adj_rf, col_gb_q4_adj_reserve, col_gc_q4_adj_comment, col_gd_q4_corrected_plan, col_gf_q4_pay_date, col_gg_q4_pay_amount, col_gh_q4_pay_ho, col_gi_q4_pay_rf, col_gj_q4_pay_comment, col_gk_q4_pay_act, col_gm_q4_booking, col_gn_q4_actual_m1, col_go_q4_actual_m2, col_gp_q4_actual_m3, col_gq_q4_actual_quarter, col_gr_q4_actual_spod, col_gs_q4_rem_booking, col_gt_q4_rem_actual, col_gv_q4_economy, col_gw_q4_total, col_gz_fact_year, col_hb_pay_year, _sort_path, _k) + JOIN sk ON sk.k = sub._k + ORDER BY sk.rnk; + +END; +$function$ +; + CREATE OR REPLACE FUNCTION v3.v_form2_sheet_jsonb(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[], p_focus_lines integer[] DEFAULT NULL::integer[]) RETURNS TABLE(row_type character varying, depth integer, line_id integer, header jsonb, plan_data jsonb, seq_dfip_data jsonb, seq_ssp_data jsonb, approved_data jsonb, contract_data jsonb, booking_data jsonb, contract_summary jsonb, allocation_data jsonb, collegial_data jsonb, ckk_data jsonb, q1_data jsonb, q2_data jsonb, q3_data jsonb, q4_data jsonb, totals_data jsonb, _sort_path integer[], _sort_rank bigint) LANGUAGE sql @@ -1022,6 +1827,268 @@ AS $function$ $function$ ; +CREATE OR REPLACE FUNCTION v3.v_form1_sheet_jsonb(p_form_id integer, p_sheet character varying, p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[], p_focus_lines integer[] DEFAULT NULL::integer[]) + RETURNS TABLE(row_type character varying, depth integer, line_id integer, header jsonb, plan_data jsonb, contract_summary jsonb, allocation_data jsonb, sequestration_data jsonb, reserve_data jsonb, approved_data jsonb, collegial_data jsonb, ckk_data jsonb, contract_detail jsonb, q1_data jsonb, q2_data jsonb, q3_data jsonb, q4_data jsonb, totals_data jsonb, _sort_path integer[], _sort_rank bigint) + LANGUAGE sql + STABLE + SET search_path TO 'v3', 'pg_catalog' +AS $function$ + WITH flags AS ( + SELECT + (p_sections IS NULL OR 'plan' = ANY(p_sections)) AS s_plan, + (p_sections IS NULL OR 'contract_summary' = ANY(p_sections)) AS s_cs, + (p_sections IS NULL OR 'allocation' = ANY(p_sections)) AS s_al, + (p_sections IS NULL OR 'sequestration' = ANY(p_sections)) AS s_seq, + (p_sections IS NULL OR 'reserve' = ANY(p_sections)) AS s_res, + (p_sections IS NULL OR 'approved' = ANY(p_sections)) AS s_appr, + (p_sections IS NULL OR 'collegial' = ANY(p_sections)) AS s_ca, + (p_sections IS NULL OR 'ckk' = ANY(p_sections)) AS s_ckk, + (p_sections IS NULL OR 'contract_detail' = ANY(p_sections)) AS s_cd, + (p_sections IS NULL OR 'q1' = ANY(p_sections)) AS s_q1, + (p_sections IS NULL OR 'q2' = ANY(p_sections)) AS s_q2, + (p_sections IS NULL OR 'q3' = ANY(p_sections)) AS s_q3, + (p_sections IS NULL OR 'q4' = ANY(p_sections)) AS s_q4, + (p_sections IS NULL OR 'totals' = ANY(p_sections)) AS s_tot + ) + SELECT + s.row_type, s.depth, s.line_id, + + jsonb_build_object( + 'year', s.col_B_year, + 'section', s.col_C_section, + 'item_id', s.col_D_item_id, + 'num_group', s.col_E_num_group, + 'name', s.col_F_name, + 'internal_order', s.col_G_internal_order, + 'expense_item_id', CASE WHEN s.row_type = 'INPUT' + THEN s._sort_path[array_upper(s._sort_path,1)-1] + ELSE s._sort_path[array_upper(s._sort_path,1)] END + ) AS header, + + CASE WHEN f.s_plan THEN jsonb_build_object( + 'q1', s.col_H_plan_q1, + 'q2', s.col_I_plan_q2, + 'q3', s.col_J_plan_q3, + 'q4', s.col_K_plan_q4, + 'year', s.col_L_plan_year, + 'comment', s.col_M_plan_comment + ) END AS plan_data, + + CASE WHEN f.s_cs THEN jsonb_build_object( + 'total', s.col_N_cs_total, + 'counterparty', s.col_O_cs_counterparty, + 'deadline', s.col_P_cs_deadline, + 'comment', s.col_Q_cs_comment, + 'future_y1', s.col_R_cs_future_y1, + 'future_y2', s.col_S_cs_future_y2, + 'other_ssp', s.col_T_cs_other_ssp + ) END AS contract_summary, + + CASE WHEN f.s_al THEN jsonb_build_object( + 'order', s.col_V_al_order, + 'property', s.col_W_al_property + ) END AS allocation_data, + + CASE WHEN f.s_seq THEN jsonb_build_object( + 'q1', s.col_Y_seq_q1, + 'q2', s.col_Z_seq_q2, + 'q3', s.col_AA_seq_q3, + 'q4', s.col_AB_seq_q4, + 'year', s.col_AC_seq_year, + 'justification', s.col_AD_seq_justification + ) END AS sequestration_data, + + CASE WHEN f.s_res THEN jsonb_build_object( + 'q1', s.col_AF_res_q1, + 'q2', s.col_AG_res_q2, + 'q3', s.col_AH_res_q3, + 'q4', s.col_AI_res_q4, + 'year', s.col_AJ_res_year, + 'justification', s.col_AK_res_justification + ) END AS reserve_data, + + CASE WHEN f.s_appr THEN jsonb_build_object( + 'q1', s.col_AM_approved_q1, + 'q2', s.col_AN_approved_q2, + 'q3', s.col_AO_approved_q3, + 'q4', s.col_AP_approved_q4, + 'year', s.col_AQ_approved_year + ) END AS approved_data, + + CASE WHEN f.s_ca THEN jsonb_build_object( + 'approved', s.col_AS_ca_approved, + 'protocol', s.col_AT_ca_protocol, + 'note', s.col_AU_ca_note + ) END AS collegial_data, + + CASE WHEN f.s_ckk THEN jsonb_build_object( + 'ceiling', s.col_AW_ckk_ceiling, + 'q1', s.col_AX_ckk_exp_q1, + 'q2', s.col_AY_ckk_exp_q2, + 'q3', s.col_AZ_ckk_exp_q3, + 'q4', s.col_BA_ckk_exp_q4, + 'rf_schedule', s.col_BB_ckk_rf_sched, + 'deadline', s.col_BC_ckk_deadline, + 'proc_plan', s.col_BD_ckk_proc_plan, + 'proc_method', s.col_BE_ckk_proc_meth, + 'comment', s.col_BF_ckk_comment + ) END AS ckk_data, + + CASE WHEN f.s_cd THEN jsonb_build_object( + 'counterparty', s.col_BH_cd_counterparty, + 'reference', s.col_BI_cd_reference, + 'addenda', s.col_BJ_cd_addenda, + 'subject', s.col_BK_cd_subject, + 'currency', s.col_BL_cd_currency, + 'ceiling', s.col_BM_cd_ceiling, + 'q1', s.col_BN_cd_exp_q1, + 'q2', s.col_BO_cd_exp_q2, + 'q3', s.col_BP_cd_exp_q3, + 'q4', s.col_BQ_cd_exp_q4, + 'rf_schedule', s.col_BR_cd_rf_sched, + 'vat_rate', s.col_BS_cd_vat, + 'exchange_rate', s.col_BT_cd_rate, + 'amount_foreign', s.col_BU_cd_amount_fx, + 'deadline', s.col_BV_cd_deadline, + 'payment_scheme', s.col_BW_cd_scheme, + 'act', s.col_BX_cd_act, + 'comment', s.col_BY_cd_comment + ) END AS contract_detail, + + CASE WHEN f.s_q1 THEN jsonb_build_object( + 'adj_current', s.col_CA_q1_adj_current, + 'adj_ssp', s.col_CB_q1_adj_ssp, + 'adj_rf', s.col_CC_q1_adj_rf, + 'adj_reserve', s.col_CD_q1_adj_reserve, + 'adj_comment', s.col_CE_q1_adj_comment, + 'corrected_plan', s.col_CF_q1_corrected_plan, + 'pay_date', TO_CHAR(s.col_CH_q1_pay_date, 'DD.MM.YYYY'), + 'pay_amount', s.col_CI_q1_pay_amount, + 'pay_ho', s.col_CJ_q1_pay_ho, + 'pay_rf', s.col_CK_q1_pay_rf, + 'pay_comment', s.col_CL_q1_pay_comment, + 'pay_act', s.col_CM_q1_pay_act, + 'booking', s.col_CO_q1_booking, + 'actual_m1', s.col_CP_q1_actual_m1, + 'actual_m2', s.col_CQ_q1_actual_m2, + 'actual_m3', s.col_CR_q1_actual_m3, + 'actual_quarter', s.col_CS_q1_actual_quarter, + 'rem_booking', s.col_CT_q1_rem_booking, + 'rem_actual', s.col_CU_q1_rem_actual, + 'transfer_q2', s.col_CW_q1_trans_q2, + 'transfer_q3', s.col_CX_q1_trans_q3, + 'transfer_q4', s.col_CY_q1_trans_q4, + 'economy', s.col_CZ_q1_economy, + 'total', s.col_DA_q1_total + ) END AS q1_data, + + CASE WHEN f.s_q2 THEN jsonb_build_object( + 'rev_eco', s.col_DC_q2_rev_eco, + 'rev_item', s.col_DD_q2_rev_item, + 'rev_inc', s.col_DE_q2_rev_inc, + 'rev_seq', s.col_DF_q2_rev_seq, + 'rev_comment', s.col_DG_q2_rev_comment, + 'new_plan', s.col_DH_q2_new_plan, + 'adj_current', s.col_DJ_q2_adj_current, + 'adj_ssp', s.col_DK_q2_adj_ssp, + 'adj_rf', s.col_DL_q2_adj_rf, + 'adj_reserve', s.col_DM_q2_adj_reserve, + 'adj_comment', s.col_DN_q2_adj_comment, + 'corrected_plan', s.col_DO_q2_corrected_plan, + 'pay_date', TO_CHAR(s.col_DQ_q2_pay_date, 'DD.MM.YYYY'), + 'pay_amount', s.col_DR_q2_pay_amount, + 'pay_ho', s.col_DS_q2_pay_ho, + 'pay_rf', s.col_DT_q2_pay_rf, + 'pay_comment', s.col_DU_q2_pay_comment, + 'pay_act', s.col_DV_q2_pay_act, + 'booking', s.col_DX_q2_booking, + 'actual_m1', s.col_DY_q2_actual_m1, + 'actual_m2', s.col_DZ_q2_actual_m2, + 'actual_m3', s.col_EA_q2_actual_m3, + 'actual_quarter', s.col_EB_q2_actual_quarter, + 'rem_booking', s.col_EC_q2_rem_booking, + 'rem_actual', s.col_ED_q2_rem_actual, + 'transfer_q3', s.col_EF_q2_trans_q3, + 'transfer_q4', s.col_EG_q2_trans_q4, + 'economy', s.col_EH_q2_economy, + 'total', s.col_EI_q2_total + ) END AS q2_data, + + CASE WHEN f.s_q3 THEN jsonb_build_object( + 'rev_eco', s.col_EK_q3_rev_eco, + 'rev_item', s.col_EL_q3_rev_item, + 'rev_inc', s.col_EM_q3_rev_inc, + 'rev_seq', s.col_EN_q3_rev_seq, + 'rev_comment', s.col_EO_q3_rev_comment, + 'new_plan', s.col_EP_q3_new_plan, + 'adj_current', s.col_ER_q3_adj_current, + 'adj_ssp', s.col_ES_q3_adj_ssp, + 'adj_rf', s.col_ET_q3_adj_rf, + 'adj_reserve', s.col_EU_q3_adj_reserve, + 'adj_comment', s.col_EV_q3_adj_comment, + 'corrected_plan', s.col_EW_q3_corrected_plan, + 'pay_date', TO_CHAR(s.col_EY_q3_pay_date, 'DD.MM.YYYY'), + 'pay_amount', s.col_EZ_q3_pay_amount, + 'pay_ho', s.col_FA_q3_pay_ho, + 'pay_rf', s.col_FB_q3_pay_rf, + 'pay_comment', s.col_FC_q3_pay_comment, + 'pay_act', s.col_FD_q3_pay_act, + 'booking', s.col_FF_q3_booking, + 'actual_m1', s.col_FG_q3_actual_m1, + 'actual_m2', s.col_FH_q3_actual_m2, + 'actual_m3', s.col_FI_q3_actual_m3, + 'actual_quarter', s.col_FJ_q3_actual_quarter, + 'rem_booking', s.col_FK_q3_rem_booking, + 'rem_actual', s.col_FL_q3_rem_actual, + 'transfer_q4', s.col_FN_q3_trans_q4, + 'economy', s.col_FO_q3_economy, + 'total', s.col_FP_q3_total + ) END AS q3_data, + + CASE WHEN f.s_q4 THEN jsonb_build_object( + 'rev_eco', s.col_FR_q4_rev_eco, + 'rev_item', s.col_FS_q4_rev_item, + 'rev_inc', s.col_FT_q4_rev_inc, + 'rev_seq', s.col_FU_q4_rev_seq, + 'rev_comment', s.col_FV_q4_rev_comment, + 'new_plan', s.col_FW_q4_new_plan, + 'adj_current', s.col_FY_q4_adj_current, + 'adj_ssp', s.col_FZ_q4_adj_ssp, + 'adj_rf', s.col_GA_q4_adj_rf, + 'adj_reserve', s.col_GB_q4_adj_reserve, + 'adj_comment', s.col_GC_q4_adj_comment, + 'corrected_plan', s.col_GD_q4_corrected_plan, + 'pay_date', TO_CHAR(s.col_GF_q4_pay_date, 'DD.MM.YYYY'), + 'pay_amount', s.col_GG_q4_pay_amount, + 'pay_ho', s.col_GH_q4_pay_ho, + 'pay_rf', s.col_GI_q4_pay_rf, + 'pay_comment', s.col_GJ_q4_pay_comment, + 'pay_act', s.col_GK_q4_pay_act, + 'booking', s.col_GM_q4_booking, + 'actual_m1', s.col_GN_q4_actual_m1, + 'actual_m2', s.col_GO_q4_actual_m2, + 'actual_m3', s.col_GP_q4_actual_m3, + 'actual_quarter', s.col_GQ_q4_actual_quarter, + 'actual_spod', s.col_GR_q4_actual_spod, + 'rem_booking', s.col_GS_q4_rem_booking, + 'rem_actual', s.col_GT_q4_rem_actual, + 'economy', s.col_GV_q4_economy, + 'total', s.col_GW_q4_total + ) END AS q4_data, + + CASE WHEN f.s_tot THEN jsonb_build_object( + 'fact_year', s.col_GZ_fact_year, + 'pay_year', s.col_HB_pay_year + ) END AS totals_data, + + s._sort_path, + s._sort_rank + FROM v_form1_sheet_sections(p_form_id, p_sheet, p_direction, p_sections, p_focus_lines) s + CROSS JOIN flags f + ORDER BY s._sort_rank; +$function$ +; + CREATE OR REPLACE FUNCTION v3.v_form2_view(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[], p_focus_lines integer[] DEFAULT NULL::integer[]) RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE plpgsql @@ -1408,3 +2475,154 @@ BEGIN END; $function$ ; + +CREATE OR REPLACE FUNCTION v3.v_form1_view(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[], p_direction character varying DEFAULT NULL::character varying, p_focus_lines integer[] DEFAULT NULL::integer[]) + RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) + LANGUAGE plpgsql + STABLE +AS $function$ +BEGIN + IF p_sheet NOT IN ('AHR','CAP','OPER') THEN + RAISE EXCEPTION 'v_form1_view: unsupported sheet %', p_sheet; + END IF; + + RETURN QUERY + SELECT + j.row_type, + j.depth, + j._sort_rank AS sort_order, + jsonb_build_object( + 'line_id', j.line_id, + 'header', j.header, + 'plan', j.plan_data, + 'contract_summary', j.contract_summary, + 'allocation', j.allocation_data, + 'sequestration', j.sequestration_data, + 'reserve', j.reserve_data, + 'approved', j.approved_data, + 'collegial', j.collegial_data, + 'ckk', j.ckk_data, + 'contract_detail', j.contract_detail, + 'q1', j.q1_data, + 'q2', j.q2_data, + 'q3', j.q3_data, + 'q4', j.q4_data, + 'totals', j.totals_data + ) AS data + FROM v3.v_form1_sheet_jsonb(p_form_id, p_sheet, p_direction, p_sections, p_focus_lines) j + ORDER BY j._sort_rank; + RETURN; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION v3.v_form_view(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[], p_direction character varying DEFAULT NULL::character varying, p_user_id integer DEFAULT NULL::integer) + RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) + LANGUAGE plpgsql + STABLE +AS $function$ +DECLARE + v_form_type VARCHAR; + v_year INT; + v_org_unit_id INT; + v_user_id INT; +BEGIN + SELECT form_type_code, year, org_unit_id + INTO v_form_type, v_year, v_org_unit_id + FROM v3.budget_form + WHERE id = p_form_id; + + IF NOT FOUND THEN + RAISE EXCEPTION 'budget_form id=% не существует в v3.budget_form', p_form_id; + END IF; + + v_user_id := p_user_id; + IF p_user_id IS NULL THEN + v_user_id := v3.current_user_id(); + END IF; + RETURN QUERY + SELECT + 'META'::VARCHAR, + -1, + 0::BIGINT, + jsonb_build_object( + 'user_id', v_user_id, + 'role', v3.user_role_code(v_user_id), + 'sheet', p_sheet, + 'editable', COALESCE( + (SELECT jsonb_agg( + jsonb_build_object('column', ec.column_key, + 'closes_at', ec.closes_at) + ORDER BY ec.column_key) + FROM v3.editable_columns_for(p_form_id, v_user_id, p_sheet, p_direction) ec), + '[]'::JSONB) + ); + + IF v_form_type = 'FORM_1' THEN + + IF p_sheet = 'SMETA' THEN + RETURN QUERY + SELECT + sm.row_type, + sm.depth, + ROW_NUMBER() OVER (ORDER BY sm.section_code) AS sort_order, + jsonb_build_object( + 'section_code', sm.section_code, + 'name', sm.name, + 'plan', jsonb_build_object( + 'support', jsonb_build_object('q1', sm.supp_plan_q1, 'q2', sm.supp_plan_q2, + 'q3', sm.supp_plan_q3, 'q4', sm.supp_plan_q4, + 'year', sm.supp_plan_year), + 'development', jsonb_build_object('q1', sm.dev_plan_q1, 'q2', sm.dev_plan_q2, + 'q3', sm.dev_plan_q3, 'q4', sm.dev_plan_q4, + 'year', sm.dev_plan_year), + 'total_year', sm.total_plan_year), + 'approved', jsonb_build_object( + 'support', jsonb_build_object('q1', sm.supp_appr_q1, 'q2', sm.supp_appr_q2, + 'q3', sm.supp_appr_q3, 'q4', sm.supp_appr_q4, + 'year', sm.supp_appr_year), + 'development', jsonb_build_object('q1', sm.dev_appr_q1, 'q2', sm.dev_appr_q2, + 'q3', sm.dev_appr_q3, 'q4', sm.dev_appr_q4, + 'year', sm.dev_appr_year), + 'total_year', sm.total_appr_year), + 'fact', jsonb_build_object( + 'support', jsonb_build_object('q1', sm.supp_act_q1, 'q2', sm.supp_act_q2, + 'q3', sm.supp_act_q3, 'q4', sm.supp_act_q4, + 'year', sm.supp_act_year), + 'development', jsonb_build_object('q1', sm.dev_act_q1, 'q2', sm.dev_act_q2, + 'q3', sm.dev_act_q3, 'q4', sm.dev_act_q4, + 'year', sm.dev_act_year), + 'total_year', sm.total_act_year), + 'corrected', jsonb_build_object( + 'support', jsonb_build_object('q2', sm.supp_corr_q2, 'q3', sm.supp_corr_q3, 'q4', sm.supp_corr_q4), + 'development', jsonb_build_object('q2', sm.dev_corr_q2, 'q3', sm.dev_corr_q3, 'q4', sm.dev_corr_q4)) + ) + FROM v3.v_form1_smeta(v_year, v_org_unit_id) sm + ORDER BY sm.section_code; + RETURN; + END IF; + + IF p_sheet IN ('AHR','CAP','OPER') THEN + RETURN QUERY SELECT * FROM v3.v_form1_view(p_form_id, p_sheet, p_sections, p_direction, NULL); + RETURN; + END IF; + + RAISE EXCEPTION 'Unknown FORM_1 sheet: %', p_sheet + USING HINT = 'Use AHR / CAP / OPER / SMETA'; + END IF; + + IF v_form_type = 'FORM_2' THEN + RETURN QUERY SELECT * FROM v3.v_form2_view(p_form_id, p_sheet, p_sections); + RETURN; + END IF; + + IF v_form_type = 'FORM_4' THEN + RETURN QUERY SELECT * FROM v3.v_form4_view(p_form_id, p_sheet, p_sections); + RETURN; + END IF; + + RAISE EXCEPTION 'Unsupported form_type=% for form_id=%', v_form_type, p_form_id; + +END; +$function$ +; -- 2.47.2 From 66b8b8a8e05f44b80b7482111b7dc7b2ccb4b93c Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 29 Oct 2026 00:06:52 +0400 Subject: [PATCH 14/20] =?UTF-8?q?perf(sql):=20=5Fform=5Faffected=5Frows=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B1=D0=B8=D1=80=D0=B0=D0=B5=D1=82=20=D1=82=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=B7=D0=B0=D1=82=D1=80=D0=BE=D0=BD?= =?UTF-8?q?=D1=83=D1=82=D0=BE=D0=B5=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D0=BE=20(AURORA-1850)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../sql/0051_upd_form_cells_subtree.sql | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql b/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql index 9bdc175..5f8befe 100644 --- a/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql +++ b/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql @@ -2626,3 +2626,117 @@ BEGIN END; $function$ ; + +CREATE OR REPLACE FUNCTION v3._form_affected_rows(p_form_id integer, p_sheet character varying, p_ids integer[], p_direction character varying DEFAULT NULL::character varying, p_sections text[] DEFAULT NULL::text[]) + RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) + LANGUAGE plpgsql + STABLE +AS $function$ +DECLARE + v_form_type VARCHAR; + v_strip TEXT[] := ARRAY[]::TEXT[]; + v_lc_path INT[]; + v_eid_set INT[]; + v_ei_path INT[]; +BEGIN + SELECT bf.form_type_code INTO v_form_type + FROM v3.budget_form bf + WHERE bf.id = p_form_id; + + IF p_sections IS NOT NULL THEN + SELECT COALESCE(array_agg(k), ARRAY[]::TEXT[]) + INTO v_strip + FROM unnest(ARRAY['plan','seq_dfip','seq_ssp','approved','contract','booking', + 'contract_summary','allocation','collegial','ckk', + 'sequestration','reserve','contract_detail', + 'q1','q2','q3','q4','totals', + 'fact_q1','fact_q2','fact_q3','fact_q4']) AS k + WHERE NOT (k = ANY(p_sections)); + END IF; + + IF v_form_type = 'FORM_1' AND p_sheet IN ('AHR','CAP','OPER') THEN + RETURN QUERY + SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip + FROM v3.v_form1_view(p_form_id, p_sheet, p_sections, p_direction, COALESCE(p_ids, '{}'::INT[])) v + ORDER BY v.sort_order; + RETURN; + ELSIF v_form_type = 'FORM_2' AND p_sheet IN ('AHR','CAP','OPER','AHR_RENT','AHR_UTILITY','AHR_SECURITY','AHR_LIMIT','OTCH9F') THEN + RETURN QUERY + SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip + FROM v3.v_form2_view(p_form_id, p_sheet, p_sections, COALESCE(p_ids, '{}'::INT[])) v + ORDER BY v.sort_order; + RETURN; + END IF; + + IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN + RETURN QUERY + SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip + FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v + WHERE v.row_type = 'INPUT' AND (v.data->>'id')::INT = ANY(p_ids) + ORDER BY v.sort_order; + + ELSIF p_sheet = 'AHR_LIMIT' THEN + v_lc_path := v3._limit_template_path_set(p_ids); + RETURN QUERY + SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip + FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v + WHERE (v.data->>'id')::INT = ANY(v_lc_path) + ORDER BY v.sort_order; + + ELSIF p_sheet = 'OTCH9F' THEN + RETURN QUERY + SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip + FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v + WHERE EXISTS ( + SELECT 1 FROM v3.fixed_asset_report far + WHERE far.id = ANY(p_ids) + AND far.expense_item_id = (v.data->>'expense_item_id')::INT + AND far.equipment_name IS NOT DISTINCT FROM (v.data->>'equipment_name')) + ORDER BY v.sort_order; + + ELSE + SELECT array_agg(DISTINCT bl.expense_item_id) + INTO v_eid_set + FROM v3.budget_line bl + WHERE bl.id = ANY(p_ids); + v_ei_path := v3._expense_item_path_set(v_eid_set); + + IF v_form_type = 'FORM_4' THEN + RETURN QUERY + WITH vv AS MATERIALIZED ( + SELECT f.row_type, f.depth, f.sort_order, f.data + FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) f + WHERE f.row_type <> 'META' + ), + chg AS ( + SELECT DISTINCT (x.data->'header'->>'program_id')::INT AS program_id, + (x.data->'header'->>'project_id')::INT AS project_id + FROM vv x + WHERE x.row_type = 'INPUT' AND (x.data->>'line_id')::INT = ANY(p_ids) + ) + SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip + FROM vv v + WHERE (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = ANY(p_ids)) + OR (v.row_type = 'ROOT' + AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)) + OR (v.row_type = 'GROUP' + AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path) + AND (v.data->'header'->>'program_id')::INT IN (SELECT c.program_id FROM chg c)) + OR (v.row_type = 'ITEM' + AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path) + AND (v.data->'header'->>'project_id')::INT IN (SELECT c.project_id FROM chg c)) + ORDER BY v.sort_order; + ELSE + RETURN QUERY + SELECT v.row_type, v.depth, v.sort_order, v.data - v_strip + FROM v3.v_form_view(p_form_id, p_sheet, p_sections, p_direction) v + WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM','SUB_SUB_ITEM') + AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)) + OR (v.row_type = 'INPUT' + AND (v.data->>'line_id')::INT = ANY(p_ids)) + ORDER BY v.sort_order; + END IF; + END IF; +END; +$function$ +; -- 2.47.2 From c236dc0e81923a7e4c31806cbe9df8a46b62ae91 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 29 Oct 2026 00:10:01 +0400 Subject: [PATCH 15/20] =?UTF-8?q?perf(sql):=20=D0=B4=D0=B5=D0=BB=D1=8C?= =?UTF-8?q?=D1=82=D0=B0=20=D0=90=D0=A5=D0=A0=20=D1=87=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=20=5Fform=5Faffected=5Frows,=20=D1=81=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=B4=D0=BA=D0=B0=D0=BC=D0=B8=20SUB=5FSUB=5FITEM=20(AURO?= =?UTF-8?q?RA-1850)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../sql/0051_upd_form_cells_subtree.sql | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql b/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql index 5f8befe..53f754d 100644 --- a/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql +++ b/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql @@ -2740,3 +2740,50 @@ BEGIN END; $function$ ; + + +CREATE OR REPLACE FUNCTION v3.get_form2_ahr_satellite_delta(p_form_id integer, p_source_sheet character varying, p_source_row_id integer) + RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) + LANGUAGE sql + STABLE +AS $function$ + WITH target_lines AS ( + SELECT m.target_line_id AS line_id + FROM v3.form2_ahr_satellite_line_source m + JOIN v3.budget_line bl ON bl.id = m.target_line_id + WHERE m.source_sheet = p_source_sheet + AND m.source_id = p_source_row_id + AND bl.budget_form_id = p_form_id + AND p_source_sheet IN ('AHR_RENT', 'AHR_UTILITY', 'AHR_SECURITY') + + UNION + + SELECT bl.id AS line_id + FROM v3.limit_template lt + JOIN v3.expense_item ei + ON ei.sheet = 'AHR' + AND ei.direction = 'Support' + AND ( + (ei.depth = 3 AND ei.item_id = lt.expense_item_code) + OR ei.num_group_id = lt.expense_item_code + ) + JOIN v3.budget_line bl + ON bl.budget_form_id = p_form_id + AND bl.expense_item_id = ei.id + AND bl.name = regexp_replace( + lt.name, + '^[[:space:]]*-[[:space:]]*', + '' + ) + WHERE p_source_sheet = 'AHR_LIMIT' + AND lt.id = p_source_row_id + AND lt.row_type = 'LEAF' + ) + SELECT r.row_type, r.depth, r.sort_order, r.data + FROM v3._form_affected_rows( + p_form_id, 'AHR', + COALESCE((SELECT array_agg(tl.line_id) FROM target_lines tl), '{}'), + 'Support', NULL) r + ORDER BY r.sort_order; +$function$ +; -- 2.47.2 From 51b349299f15fb31f196d41493197f1e34d5ae42 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 29 Oct 2026 00:13:13 +0400 Subject: [PATCH 16/20] =?UTF-8?q?perf(sql):=20jit=20=3D=20off=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=D1=81=D1=82=D1=80=D0=BE=D0=B8=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8F=D1=85=20=D0=B8=20=D0=BF=D1=83=D1=82=D0=B8=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20(AURORA-1850)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../versions/sql/0051_upd_form_cells_subtree.sql | 4 ++++ .../integration/test_upd_form_cells_subtree.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql b/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql index 53f754d..f3fb76a 100644 --- a/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql +++ b/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql @@ -13,6 +13,7 @@ CREATE OR REPLACE FUNCTION v3.v_form2_sheet_sections(p_form_id integer, p_sheet RETURNS TABLE(row_type character varying, depth integer, line_id integer, col_section_code character varying, col_item_id character varying, col_num_group_id character varying, col_name character varying, col_vsp_id integer, col_vsp_address character varying, col_plan_q1 numeric, col_plan_q2 numeric, col_plan_q3 numeric, col_plan_q4 numeric, col_plan_year numeric, col_plan_comment character varying, col_seq_dfip_q1 numeric, col_seq_dfip_q2 numeric, col_seq_dfip_q3 numeric, col_seq_dfip_q4 numeric, col_seq_dfip_year numeric, col_seq_dfip_just character varying, col_seq_ssp_q1 numeric, col_seq_ssp_q2 numeric, col_seq_ssp_q3 numeric, col_seq_ssp_q4 numeric, col_seq_ssp_year numeric, col_seq_ssp_just character varying, col_appr_q1 numeric, col_appr_q2 numeric, col_appr_q3 numeric, col_appr_q4 numeric, col_appr_year numeric, col_cd_counterparty character varying, col_cd_reference character varying, col_cd_date date, col_cd_subject character varying, col_cd_currency character varying, col_cd_ceiling numeric, col_cd_vat_rate character varying, col_cd_deadline character varying, col_cd_scheme character varying, col_cd_act character varying, col_cd_comment character varying, col_book_q1 numeric, col_book_q2 numeric, col_book_q3 numeric, col_book_q4 numeric, col_book_next_q1 numeric, col_book_next_q2 numeric, col_book_next_q3 numeric, col_book_next_q4 numeric, col_q1_adj_current numeric, col_q1_adj_ssp numeric, col_q1_adj_reserve numeric, col_q1_adj_comment character varying, col_q1_corrected_plan numeric, col_q1_pay_date date, col_q1_pay_amount numeric, col_q1_pay_comment character varying, col_q1_pay_act character varying, col_q1_booking numeric, col_q1_actual_m1 numeric, col_q1_actual_m2 numeric, col_q1_actual_m3 numeric, col_q1_actual_quarter numeric, col_q1_residual_after_booking numeric, col_q1_residual_after_actual numeric, col_q1_transfer_q2 numeric, col_q1_transfer_q2_delay_acts numeric, col_q1_transfer_q2_delay_procurement numeric, col_q1_transfer_q2_economy_rf numeric, col_q1_transfer_next_comment character varying, col_q1_transfer_q3 numeric, col_q1_transfer_q4 numeric, col_q1_transfer_far_comment character varying, col_q1_transfer_econ numeric, col_q1_total numeric, col_q2_target_change numeric, col_q2_base_correction numeric, col_q2_base_correction_comment character varying, col_q2_revision_inc numeric, col_q2_revision_seq numeric, col_q2_revision_comment character varying, col_q2_new_plan numeric, col_q2_adj_current numeric, col_q2_adj_ssp numeric, col_q2_adj_reserve numeric, col_q2_adj_comment character varying, col_q2_corrected_plan numeric, col_q2_pay_date date, col_q2_pay_amount numeric, col_q2_pay_comment character varying, col_q2_pay_act character varying, col_q2_booking numeric, col_q2_actual_m1 numeric, col_q2_actual_m2 numeric, col_q2_actual_m3 numeric, col_q2_actual_quarter numeric, col_q2_residual_after_booking numeric, col_q2_residual_after_actual numeric, col_q2_transfer_q3 numeric, col_q2_transfer_q3_delay_acts numeric, col_q2_transfer_q3_delay_procurement numeric, col_q2_transfer_q3_economy_rf numeric, col_q2_transfer_next_comment character varying, col_q2_transfer_q4 numeric, col_q2_transfer_far_comment character varying, col_q2_transfer_econ numeric, col_q2_total numeric, col_q3_target_change numeric, col_q3_base_correction numeric, col_q3_base_correction_comment character varying, col_q3_revision_inc numeric, col_q3_revision_seq numeric, col_q3_revision_comment character varying, col_q3_new_plan numeric, col_q3_adj_current numeric, col_q3_adj_ssp numeric, col_q3_adj_reserve numeric, col_q3_adj_comment character varying, col_q3_corrected_plan numeric, col_q3_pay_date date, col_q3_pay_amount numeric, col_q3_pay_comment character varying, col_q3_pay_act character varying, col_q3_booking numeric, col_q3_actual_m1 numeric, col_q3_actual_m2 numeric, col_q3_actual_m3 numeric, col_q3_actual_quarter numeric, col_q3_residual_after_booking numeric, col_q3_residual_after_actual numeric, col_q3_transfer_q4 numeric, col_q3_transfer_q4_delay_acts numeric, col_q3_transfer_q4_delay_procurement numeric, col_q3_transfer_q4_economy_rf numeric, col_q3_transfer_next_comment character varying, col_q3_transfer_econ numeric, col_q3_total numeric, col_q4_target_change numeric, col_q4_base_correction numeric, col_q4_base_correction_comment character varying, col_q4_revision_inc numeric, col_q4_revision_seq numeric, col_q4_revision_comment character varying, col_q4_new_plan numeric, col_q4_adj_current numeric, col_q4_adj_ssp numeric, col_q4_adj_reserve numeric, col_q4_adj_comment character varying, col_q4_corrected_plan numeric, col_q4_pay_date date, col_q4_pay_amount numeric, col_q4_pay_comment character varying, col_q4_pay_act character varying, col_q4_booking numeric, col_q4_actual_m1 numeric, col_q4_actual_m2 numeric, col_q4_actual_m3 numeric, col_q4_actual_spod numeric, col_q4_actual_quarter numeric, col_q4_residual_after_booking numeric, col_q4_residual_after_actual numeric, col_q4_transfer_econ numeric, col_q4_total numeric, col_fact_year numeric, col_pay_year numeric, col_economy_year numeric, col_internal_order character varying, col_cs_total numeric, col_cs_counterparty character varying, col_cs_deadline character varying, col_cs_comment character varying, col_cs_future_y1 numeric, col_cs_future_y2 numeric, col_cs_other_ssp numeric, col_al_contract_ref character varying, col_al_purpose character varying, col_al_order character varying, col_al_property character varying, col_ca_approved numeric, col_ca_protocol character varying, col_ca_note character varying, col_ckk_ceiling numeric, col_ckk_rf_sched character varying, col_ckk_deadline character varying, col_ckk_proc_plan character varying, col_ckk_proc_meth character varying, col_ckk_comment character varying, col_cd_addenda character varying, col_cd_exp_q1 numeric, col_cd_exp_q2 numeric, col_cd_exp_q3 numeric, col_cd_exp_q4 numeric, col_cd_rf_sched character varying, col_cd_exchange_rate numeric, col_cd_amount_foreign numeric, col_q1_adj_rf numeric, col_q1_pay_ho numeric, col_q1_pay_rf numeric, col_q2_adj_rf numeric, col_q2_pay_ho numeric, col_q2_pay_rf numeric, col_q3_adj_rf numeric, col_q3_pay_ho numeric, col_q3_pay_rf numeric, col_q4_adj_rf numeric, col_q4_pay_ho numeric, col_q4_pay_rf numeric, _sort_path integer[], _sort_rank bigint) LANGUAGE plpgsql STABLE + SET jit TO 'off' AS $function$ #variable_conflict use_column DECLARE @@ -752,6 +753,7 @@ CREATE OR REPLACE FUNCTION v3.v_form1_sheet_sections(p_form_id integer, p_sheet RETURNS TABLE(row_type character varying, depth integer, line_id integer, col_b_year integer, col_c_section character varying, col_d_item_id character varying, col_e_num_group character varying, col_f_name character varying, col_g_internal_order character varying, col_h_plan_q1 numeric, col_i_plan_q2 numeric, col_j_plan_q3 numeric, col_k_plan_q4 numeric, col_l_plan_year numeric, col_m_plan_comment character varying, col_n_cs_total numeric, col_o_cs_counterparty character varying, col_p_cs_deadline character varying, col_q_cs_comment character varying, col_r_cs_future_y1 numeric, col_s_cs_future_y2 numeric, col_t_cs_other_ssp numeric, col_v_al_order character varying, col_w_al_property character varying, col_y_seq_q1 numeric, col_z_seq_q2 numeric, col_aa_seq_q3 numeric, col_ab_seq_q4 numeric, col_ac_seq_year numeric, col_ad_seq_justification character varying, col_af_res_q1 numeric, col_ag_res_q2 numeric, col_ah_res_q3 numeric, col_ai_res_q4 numeric, col_aj_res_year numeric, col_ak_res_justification character varying, col_am_approved_q1 numeric, col_an_approved_q2 numeric, col_ao_approved_q3 numeric, col_ap_approved_q4 numeric, col_aq_approved_year numeric, col_as_ca_approved numeric, col_at_ca_protocol character varying, col_au_ca_note character varying, col_aw_ckk_ceiling numeric, col_ax_ckk_exp_q1 numeric, col_ay_ckk_exp_q2 numeric, col_az_ckk_exp_q3 numeric, col_ba_ckk_exp_q4 numeric, col_bb_ckk_rf_sched character varying, col_bc_ckk_deadline character varying, col_bd_ckk_proc_plan character varying, col_be_ckk_proc_meth character varying, col_bf_ckk_comment character varying, col_bh_cd_counterparty character varying, col_bi_cd_reference character varying, col_bj_cd_addenda character varying, col_bk_cd_subject character varying, col_bl_cd_currency character varying, col_bm_cd_ceiling numeric, col_bn_cd_exp_q1 numeric, col_bo_cd_exp_q2 numeric, col_bp_cd_exp_q3 numeric, col_bq_cd_exp_q4 numeric, col_br_cd_rf_sched character varying, col_bs_cd_vat character varying, col_bt_cd_rate numeric, col_bu_cd_amount_fx numeric, col_bv_cd_deadline character varying, col_bw_cd_scheme character varying, col_bx_cd_act character varying, col_by_cd_comment character varying, col_ca_q1_adj_current numeric, col_cb_q1_adj_ssp numeric, col_cc_q1_adj_rf numeric, col_cd_q1_adj_reserve numeric, col_ce_q1_adj_comment character varying, col_cf_q1_corrected_plan numeric, col_ch_q1_pay_date date, col_ci_q1_pay_amount numeric, col_cj_q1_pay_ho numeric, col_ck_q1_pay_rf numeric, col_cl_q1_pay_comment character varying, col_cm_q1_pay_act character varying, col_co_q1_booking numeric, col_cp_q1_actual_m1 numeric, col_cq_q1_actual_m2 numeric, col_cr_q1_actual_m3 numeric, col_cs_q1_actual_quarter numeric, col_ct_q1_rem_booking numeric, col_cu_q1_rem_actual numeric, col_cw_q1_trans_q2 numeric, col_cx_q1_trans_q3 numeric, col_cy_q1_trans_q4 numeric, col_cz_q1_economy numeric, col_da_q1_total numeric, col_dc_q2_rev_eco numeric, col_dd_q2_rev_item numeric, col_de_q2_rev_inc numeric, col_df_q2_rev_seq numeric, col_dg_q2_rev_comment character varying, col_dh_q2_new_plan numeric, col_dj_q2_adj_current numeric, col_dk_q2_adj_ssp numeric, col_dl_q2_adj_rf numeric, col_dm_q2_adj_reserve numeric, col_dn_q2_adj_comment character varying, col_do_q2_corrected_plan numeric, col_dq_q2_pay_date date, col_dr_q2_pay_amount numeric, col_ds_q2_pay_ho numeric, col_dt_q2_pay_rf numeric, col_du_q2_pay_comment character varying, col_dv_q2_pay_act character varying, col_dx_q2_booking numeric, col_dy_q2_actual_m1 numeric, col_dz_q2_actual_m2 numeric, col_ea_q2_actual_m3 numeric, col_eb_q2_actual_quarter numeric, col_ec_q2_rem_booking numeric, col_ed_q2_rem_actual numeric, col_ef_q2_trans_q3 numeric, col_eg_q2_trans_q4 numeric, col_eh_q2_economy numeric, col_ei_q2_total numeric, col_ek_q3_rev_eco numeric, col_el_q3_rev_item numeric, col_em_q3_rev_inc numeric, col_en_q3_rev_seq numeric, col_eo_q3_rev_comment character varying, col_ep_q3_new_plan numeric, col_er_q3_adj_current numeric, col_es_q3_adj_ssp numeric, col_et_q3_adj_rf numeric, col_eu_q3_adj_reserve numeric, col_ev_q3_adj_comment character varying, col_ew_q3_corrected_plan numeric, col_ey_q3_pay_date date, col_ez_q3_pay_amount numeric, col_fa_q3_pay_ho numeric, col_fb_q3_pay_rf numeric, col_fc_q3_pay_comment character varying, col_fd_q3_pay_act character varying, col_ff_q3_booking numeric, col_fg_q3_actual_m1 numeric, col_fh_q3_actual_m2 numeric, col_fi_q3_actual_m3 numeric, col_fj_q3_actual_quarter numeric, col_fk_q3_rem_booking numeric, col_fl_q3_rem_actual numeric, col_fn_q3_trans_q4 numeric, col_fo_q3_economy numeric, col_fp_q3_total numeric, col_fr_q4_rev_eco numeric, col_fs_q4_rev_item numeric, col_ft_q4_rev_inc numeric, col_fu_q4_rev_seq numeric, col_fv_q4_rev_comment character varying, col_fw_q4_new_plan numeric, col_fy_q4_adj_current numeric, col_fz_q4_adj_ssp numeric, col_ga_q4_adj_rf numeric, col_gb_q4_adj_reserve numeric, col_gc_q4_adj_comment character varying, col_gd_q4_corrected_plan numeric, col_gf_q4_pay_date date, col_gg_q4_pay_amount numeric, col_gh_q4_pay_ho numeric, col_gi_q4_pay_rf numeric, col_gj_q4_pay_comment character varying, col_gk_q4_pay_act character varying, col_gm_q4_booking numeric, col_gn_q4_actual_m1 numeric, col_go_q4_actual_m2 numeric, col_gp_q4_actual_m3 numeric, col_gq_q4_actual_quarter numeric, col_gr_q4_actual_spod numeric, col_gs_q4_rem_booking numeric, col_gt_q4_rem_actual numeric, col_gv_q4_economy numeric, col_gw_q4_total numeric, col_gz_fact_year numeric, col_hb_pay_year numeric, _sort_path integer[], _sort_rank bigint) LANGUAGE plpgsql STABLE + SET jit TO 'off' SET search_path TO 'v3', 'pg_catalog' AS $function$ #variable_conflict use_column @@ -2631,6 +2633,7 @@ CREATE OR REPLACE FUNCTION v3._form_affected_rows(p_form_id integer, p_sheet cha RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE plpgsql STABLE + SET jit TO 'off' AS $function$ DECLARE v_form_type VARCHAR; @@ -2746,6 +2749,7 @@ CREATE OR REPLACE FUNCTION v3.get_form2_ahr_satellite_delta(p_form_id integer, p RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb) LANGUAGE sql STABLE + SET jit TO 'off' AS $function$ WITH target_lines AS ( SELECT m.target_line_id AS line_id diff --git a/api/tests/integration/test_upd_form_cells_subtree.py b/api/tests/integration/test_upd_form_cells_subtree.py index faa850c..dda8bde 100644 --- a/api/tests/integration/test_upd_form_cells_subtree.py +++ b/api/tests/integration/test_upd_form_cells_subtree.py @@ -452,3 +452,18 @@ def test_ahr_delta_includes_sub_sub_item_for_depth4_limit(client, headers, db_ex assert any( row[0] == "SUB_SUB_ITEM" and json.loads(row[3])["header"]["expense_item_id"] == eid for row in delta ), "в дельте нет SUB_SUB_ITEM статьи глубины 4" + + +JIT_OFF_FUNCS = ("v_form2_sheet_sections", "v_form1_sheet_sections", + "_form_affected_rows", "get_form2_ahr_satellite_delta") + + +def test_jit_off_on_builders_and_edit_path(db_exec): + rows = db_exec( + """SELECT p.proname, COALESCE(p.proconfig, '{}') FROM pg_proc p + JOIN pg_namespace n ON n.oid = p.pronamespace + WHERE n.nspname = 'v3' AND p.proname = ANY(CAST(:names AS text[]))""", + names=list(JIT_OFF_FUNCS)) + assert {r[0] for r in rows} == set(JIT_OFF_FUNCS) + missing = [r[0] for r in rows if "jit=off" not in r[1]] + assert not missing, f"нет SET jit = off: {missing}" -- 2.47.2 From f56e3e94c8e0b1aa8e0770dfa8f7c3a2680edcf4 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 29 Oct 2026 00:15:13 +0400 Subject: [PATCH 17/20] =?UTF-8?q?test(sheet):=20=D0=B7=D0=B0=D1=89=D0=B8?= =?UTF-8?q?=D1=82=D0=B0=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=200037=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=200051=20(AURORA-1850)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../test_upd_form_cells_subtree_0037_guard.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 api/tests/integration/test_upd_form_cells_subtree_0037_guard.py diff --git a/api/tests/integration/test_upd_form_cells_subtree_0037_guard.py b/api/tests/integration/test_upd_form_cells_subtree_0037_guard.py new file mode 100644 index 0000000..1fec5a6 --- /dev/null +++ b/api/tests/integration/test_upd_form_cells_subtree_0037_guard.py @@ -0,0 +1,47 @@ +"""AURORA-1850: защита правки 0037 (LEAF-лимиты АХР и сопоставление num_group_id) после 0051. + +Откат 0037 — нарушение корректности: тесты должны падать, если тело функций вернулось к виду до 0037. +""" + +import re +from decimal import Decimal + +import pytest + +from test_upd_form_cells_full_rows import ( # noqa: F401 (фикстуры и хелперы) + _get_sheet, + _patch_cell, + db_exec, + headers, +) + + +def test_v_form2_view_keeps_0037_limit_formula(client, headers, db_exec): + # LEAF «- принтеры», limit_template.id = 13, код 204620030 (0013_form2_limit.sql:1707) + _patch_cell(client, headers, 4, "AHR_LIMIT", 13, "qty_q1", 4) + row = next(r for r in _get_sheet(client, headers, 4, "AHR_LIMIT") if r["data"].get("id") == 13) + assert Decimal(str(row["data"]["amount_q1"])) == Decimal(4) * Decimal("0.57") * 10900 / 4 + src = db_exec("SELECT pg_get_functiondef(CAST(:s AS regprocedure))", + s="v3.v_form2_view(integer,character varying,text[],integer[])")[0][0] + assert "_form2_ahr_limit_amount" in src + + +OLD_DEPTH3_MATCHER = re.compile(r"ei\.depth\s*=\s*3\s+AND\s*\(") # вид до 0037 + + +def test_ahr_delta_keeps_0037_num_group_matching(client, headers, db_exec): + found = db_exec( + """SELECT lt.id FROM v3.limit_template lt + JOIN v3.expense_item ei ON ei.num_group_id = lt.expense_item_code + AND ei.sheet = 'AHR' AND ei.direction = 'Support' AND ei.depth = 4 + WHERE lt.row_type = 'LEAF' ORDER BY lt.id LIMIT 1""") + if not found: + pytest.skip("нет LEAF шаблона с кодом = num_group_id статьи AHR глубины 4") + lt_id = found[0][0] + _patch_cell(client, headers, 4, "AHR_LIMIT", lt_id, "qty_q1", 4) + delta = db_exec("SELECT row_type FROM v3.get_form2_ahr_satellite_delta(4, 'AHR_LIMIT', :i)", i=lt_id) + assert any(r[0] == "INPUT" for r in delta), "дельта АХР пуста — откат сопоставления 0037" + src = db_exec("SELECT pg_get_functiondef(CAST(:s AS regprocedure))", + s="v3.get_form2_ahr_satellite_delta(integer,character varying,integer)")[0][0] + assert "ei.num_group_id = lt.expense_item_code" in src + assert not OLD_DEPTH3_MATCHER.search(src) -- 2.47.2 From 4a5e15e970d83a5f33637deb84ea5fad63082cc2 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 29 Oct 2026 00:19:04 +0400 Subject: [PATCH 18/20] =?UTF-8?q?docs:=20ARCHITECTURE=20=E2=80=94=20=D1=84?= =?UTF-8?q?=D0=BE=D0=BA=D1=83=D1=81=D0=BD=D0=B0=D1=8F=20=D1=81=D0=B1=D0=BE?= =?UTF-8?q?=D1=80=D0=BA=D0=B0=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D1=83=20(AURORA-1?= =?UTF-8?q?850)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Haiku 5.5 --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index bc57c07..b3edd31 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2,7 +2,7 @@ Монорепозиторий: `api/` (FastAPI) отдаёт REST, WebSocket и собранный фронт; `web/` — SPA на React. Основная бизнес-логика — PL/pgSQL-функции схемы `v3` в Postgres. Python-слой проверяет роли, валидирует запрос, вызывает функцию и маппит её ошибки. -Поток редактирования ячейки: `RealtimeTable` → WebSocket `/api/v1/ws/form/{id}/sheet/{sheet}` (`cell_edit_start` — блокировка, `cell_updated`) → `SheetService` → `v3.upd_form_cell(s)`. Функция проверяет роль и окно этапа и пишет аудит `v3.log_event`, после чего изменение рассылается остальным подключённым. Ответ `upd_form_cells` (HTTP и `cell_updated`) — полные строки (`data` как в GET листа, с явными `null`): изменённые INPUT и все их предки-агрегаты, в том числе неизменившиеся; при `sections` ключи исключённых разделов убираются. Выборку делает `v3._form_affected_rows` (миграция 0050). +Поток редактирования ячейки: `RealtimeTable` → WebSocket `/api/v1/ws/form/{id}/sheet/{sheet}` (`cell_edit_start` — блокировка, `cell_updated`) → `SheetService` → `v3.upd_form_cell(s)`. Функция проверяет роль и окно этапа и пишет аудит `v3.log_event`, после чего изменение рассылается остальным подключённым. Ответ `upd_form_cells` (HTTP и `cell_updated`) — полные строки (`data` как в GET листа, с явными `null`): изменённые INPUT и все их предки-агрегаты, в том числе неизменившиеся; при `sections` ключи исключённых разделов убираются. Выборку делает `v3._form_affected_rows` (миграции 0050, 0051). Для FORM_1 и FORM_2 она не собирает лист целиком: построители (`v_form1_view`/`v_form2_view` → `*_sheet_jsonb` → `*_sheet_sections`) вызываются с фокусом `p_focus_lines` (NULL — весь лист, `'{}'` — ничего) и считают только изменённые строки и узлы их путей. `sort_order` в обоих режимах берётся из скелета листа (`_sort_rank`), поэтому совпадает с GET. FORM_4 до AURORA-1846 собирается полностью. Фоновая дельта АХР после правки сателлита (`get_form2_ahr_satellite_delta`) идёт тем же путём. ## Модули -- 2.47.2 From 655a69cf340691394ed31132d432927ea736dcec Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 29 Oct 2026 00:32:29 +0400 Subject: [PATCH 19/20] =?UTF-8?q?test(sheet):=20=D0=BD=D0=B8=D1=87=D1=8C?= =?UTF-8?q?=D1=8F=20=D0=BA=D0=BB=D1=8E=D1=87=D0=B0=20=D1=81=D0=BE=D1=80?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8=20=D0=BF=D0=BE-?= =?UTF-8?q?=D0=BD=D0=B0=D1=81=D1=82=D0=BE=D1=8F=D1=89=D0=B5=D0=BC=D1=83,?= =?UTF-8?q?=20F1-AHR-plan,=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=D1=80=D0=B8=D0=B9=200051=20(AURORA-1850)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../sql/0051_upd_form_cells_subtree.sql | 4 +-- .../test_upd_form_cells_subtree.py | 31 ++++++++++++------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql b/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql index f3fb76a..2d09e32 100644 --- a/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql +++ b/api/alembic/versions/sql/0051_upd_form_cells_subtree.sql @@ -1,7 +1,5 @@ -- AURORA-1850 (change: openspec/changes/aurora-1850-upd-form-cells-subtree): правка ячейки пересчитывает только затронутое поддерево. --- Параметр фокуса p_focus_lines (D1) и колонка _sort_rank (D2) в построителях FORM_2 AHR/CAP/OPER. --- Тела — буквальные, сняты с БД после 0050 (D8, before-0051.sql); порядок операторов — D9. --- FORM_1 (Task 5): те же правки, v_form1_view, v_form_view. Остальные функции добавляются следующими задачами плана. +-- AURORA-1850: фокусные сборщики FORM_1/FORM_2, скелет с _sort_rank, _form_affected_rows и дельта AHR на фокусном пути, jit=off. DROP FUNCTION v3.v_form2_view(integer, character varying, text[]); DROP FUNCTION v3.v_form2_sheet_jsonb(integer, character varying, text[]); diff --git a/api/tests/integration/test_upd_form_cells_subtree.py b/api/tests/integration/test_upd_form_cells_subtree.py index dda8bde..0829053 100644 --- a/api/tests/integration/test_upd_form_cells_subtree.py +++ b/api/tests/integration/test_upd_form_cells_subtree.py @@ -45,6 +45,7 @@ CASES = { "F2-AHR_LIMIT": ("FORM_2", "AHR_LIMIT", None, None, 0), "F2-OTCH9F": ("FORM_2", "OTCH9F", None, None, 0), "F2-AHR-plan": ("FORM_2", "AHR", None, ["plan"], 2048), + "F1-AHR-plan": ("FORM_1", "AHR", "Support", ["plan"], 2049), } SAT_FORM = 4 @@ -201,7 +202,6 @@ def test_affected_rows_match_reference(client, headers, db_exec, path, case): form_type, sheet, direction, sections, _ = CASES[case] form_id, units, batches = _prepare(client, headers, db_exec, case) assert units, "в листе нет строк" - is_tree = sheet in ("AHR", "CAP", "OPER") for i in units: _check(db_exec, path, form_type, form_id, sheet, [i], direction, sections, f"{case} id={i}", must_have_input=sheet != "AHR_LIMIT" and sheet != "OTCH9F") @@ -209,7 +209,6 @@ def test_affected_rows_match_reference(client, headers, db_exec, path, case): dev = batches[0][-1] rows = affected_rows(db_exec, form_id, sheet, [dev], direction, sections) assert not any(_has_input(r, dev) for r in rows), "Development INPUT попал в результат Support" - del is_tree for n, batch in enumerate(batches): _check(db_exec, path, form_type, form_id, sheet, batch, direction, sections, f"{case} batch#{n}") @@ -373,17 +372,22 @@ def _sort_key_candidate(db_exec): ORDER BY b.id LIMIT 1 """ ) - return rows[0][0] if rows else None + return (rows[0][0], rows[0][1]) if rows else None def test_sort_key_tie_node_before_line(client, headers, db_exec): - child = _sort_key_candidate(db_exec) - if child is None: + cand = _sort_key_candidate(db_exec) + if cand is None: pytest.skip("нет статьи AHR глубины 3 с дочерней глубины 4 и свободным id в budget_line") + child, parent = cand + assert child != parent form_id = _new_form(client, headers, "FORM_2", 2058) + # строка висит на родительской статье, а её id равен id дочерней статьи: + # ключ сортировки INPUT совпадает с ключом узла дочерней статьи (ничья) db_exec( "INSERT INTO v3.budget_line (id, budget_form_id, expense_item_id, name) " - "VALUES (:i, :f, :i, 'tie')", i=child, f=form_id) + "VALUES (:i, :f, :p, 'tie')", i=child, f=form_id, p=parent) + assert db_exec("SELECT expense_item_id FROM v3.budget_line WHERE id = :i", i=child)[0][0] == parent g1 = _get_sheet(client, headers, form_id, "AHR") g2 = _get_sheet(client, headers, form_id, "AHR") assert [row_key(r) for r in g1] == [row_key(r) for r in g2] @@ -391,13 +395,16 @@ def test_sort_key_tie_node_before_line(client, headers, db_exec): assert idx > 0 prev = g1[idx - 1] assert prev["row_type"] in TREE_TYPES and prev["data"]["header"]["expense_item_id"] == child, prev + assert prev["sort_order"] == g1[idx]["sort_order"] - 1 patch = _patch_cell(client, headers, form_id, "AHR", child, "plan.q1", 8) - node = next((r for r in patch if r["row_type"] in TREE_TYPES - and r["data"]["header"]["expense_item_id"] == child), None) - assert node is not None, "узел дочерней статьи отсутствует в ответе" - line_row = next(r for r in patch if r["row_type"] == "INPUT" and r["data"]["line_id"] == child) - assert node["sort_order"] < line_row["sort_order"] - assert_patch_rows_match_sheet(patch, _get_sheet(client, headers, form_id, "AHR")) + get_rows = _get_sheet(client, headers, form_id, "AHR") + line_patch = next(r for r in patch if r["row_type"] == "INPUT" and r["data"]["line_id"] == child) + line_get = next(r for r in get_rows if r["row_type"] == "INPUT" and r["data"]["line_id"] == child) + assert line_patch["sort_order"] == line_get["sort_order"] + node_get = next(r for r in get_rows if r["row_type"] in TREE_TYPES + and r["data"]["header"]["expense_item_id"] == child) + assert node_get["sort_order"] == line_get["sort_order"] - 1 + assert_patch_rows_match_sheet(patch, get_rows) def _delta_equals_get(db_exec, sheet, row_id): -- 2.47.2 From b5a245e1e81cbd0a4b89248762c31597b224a888 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 29 Oct 2026 00:34:10 +0400 Subject: [PATCH 20/20] =?UTF-8?q?docs(openspec):=20aurora-1850=20=E2=80=94?= =?UTF-8?q?=20=D1=80=D1=83=D0=BB=D0=B8=D0=BD=D0=B3=20R-2=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=8F=D0=B5=D1=82=20skip=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B0=20track=5Ffunctions=20(M-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- openspec/changes/aurora-1850-upd-form-cells-subtree/design.md | 1 + 1 file changed, 1 insertion(+) diff --git a/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md b/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md index bc0ef01..d1f1ae5 100644 --- a/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md +++ b/openspec/changes/aurora-1850-upd-form-cells-subtree/design.md @@ -286,3 +286,4 @@ | 2026-10-28 | Исполнитель Task 1 (T1.2): посев снимка GET создаёт строки FORM_1 AHR/CAP и для Support, и для Development; снимок включает и фикстурные формы 1 и 4 | `expense_item.direction` везде Support, иначе ветка Development в снимке пуста | Снимок содержит чуть больше строк, чем описано в плане; на проверку корректности не влияет | | 2026-10-28 | Исполнитель Task 3 (T3.1): ранг скелета присоединяется внешним `JOIN sk` поверх `UNION` построителя, а не внутри веток; ключ скелета один bigint (узел = `-id`, строка = `lid`) | Вариант с `JOIN sk` в каждой ветке обрушил оценку строк, планировщик выбрал nested loop, GET AHR вырос со 105 до 203 мс; внешний JOIN даёт около 114 мс (+8 %) | Хрупкость плана на других объёмах; ловит замер GET на стенде | | 2026-10-28 | Исполнитель Task 4 (T4.4): JOIN `vsp` в скелете сателлитов оставлен, хотя `vsp_id` и `line_id` NOT NULL с FK (доказано по схеме); OTCH9F: перестановка строк с равным `(expense_item_id, month)` по tie-break `id` в снимке GET — 4 строки формы 26 (id 4/1 месяц 1, id 6/2 месяц 2), содержимое строк одинаково | Рулинг R-1; порядок при равных ключах раньше не был определён (D6) | Если фронт опирался на прежний случайный порядок равных ключей, порядок двух пар строк изменится | +| 2026-10-29 | Контроллер (M-3): тест «0 вложенных `v_form_view`» жёстко требует `rolsuper` (assert вместо skip) | Рулинг R-2 | На не-суперпользовательской роли тест падает; нужен суперпользователь или проверка `has_parameter_privilege` | -- 2.47.2