diff --git a/api/alembic/versions/0058_form2_ahr_delta_depth4.py b/api/alembic/versions/0058_form2_ahr_delta_depth4.py new file mode 100644 index 0000000..9f5d5f8 --- /dev/null +++ b/api/alembic/versions/0058_form2_ahr_delta_depth4.py @@ -0,0 +1,15 @@ +from sql_migration import run_sql_file_migration + + +revision = "0058" +down_revision = "0057" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + run_sql_file_migration("0058_form2_ahr_delta_depth4.sql") + + +def downgrade() -> None: + pass diff --git a/api/alembic/versions/sql/0058_form2_ahr_delta_depth4.sql b/api/alembic/versions/sql/0058_form2_ahr_delta_depth4.sql new file mode 100644 index 0000000..a81c72a --- /dev/null +++ b/api/alembic/versions/sql/0058_form2_ahr_delta_depth4.sql @@ -0,0 +1,86 @@ +-- AURORA-1850 / issue #296: дельта АХР после правки лимита пуста для статей +-- глубины 4 (принтеры / МФУ / копировальные аппараты): открытый АХР не +-- обновляет сумму до перезагрузки. +-- +-- Причина: 0053 пересоздала v3.get_form2_ahr_satellite_delta по телу из 0031 — +-- с сопоставлением лимитного шаблона «ei.depth = 3 AND (item_id = code OR +-- num_group_id = code)», то есть откатила правку 0037. По 0037 (и как во всех +-- остальных функциях синхронизации, в том числе в 0052/0053) по item_id +-- сопоставляется только статья глубины 3, а по num_group_id — статья любой +-- глубины. Лимиты с кодом num_group_id статьи глубины 4 в target_lines не +-- попадали, и дельта была пустой. +-- +-- Тело — из 0053 (с SUB_SUB_ITEM в заголовках, issue #294) + матчер 0037. +-- Позже 0053 функцию никто не переопределял (проверено по 0054–0057). +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.source_sheet = 'AHR_LIMIT' + 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', 'SUB_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$; 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 index 1fec5a6..9373eba 100644 --- a/api/tests/integration/test_upd_form_cells_subtree_0037_guard.py +++ b/api/tests/integration/test_upd_form_cells_subtree_0037_guard.py @@ -31,16 +31,23 @@ OLD_DEPTH3_MATCHER = re.compile(r"ei\.depth\s*=\s*3\s+AND\s*\(") # вид до def test_ahr_delta_keeps_0037_num_group_matching(client, headers, db_exec): found = db_exec( - """SELECT lt.id FROM v3.limit_template lt + """SELECT lt.id, ei.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] + lt_id, ei_id = found[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" + delta = db_exec( + "SELECT row_type, data->'header'->>'expense_item_id' FROM v3.get_form2_ahr_satellite_delta(4, 'AHR_LIMIT', :i)", + i=lt_id, + ) + # AURORA-1887 скрыл лимитные INPUT-строки на листе AHR — в дельте остаются + # заголовки статьи и её предков; для статьи глубины 4 это SUB_SUB_ITEM. + assert any(r[0] == "SUB_SUB_ITEM" and int(r[1]) == ei_id for r in delta), ( + f"в дельте АХР нет заголовка статьи глубины 4 — откат сопоставления 0037: {delta}" + ) 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