perf(api): свод 2 — общий отбор svod2_lines, страницы с total_count, варианты фильтров в SQL

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-28 21:40:41 +04:00
parent a62add9779
commit 719f568863
3 changed files with 750 additions and 3 deletions

View File

@ -235,3 +235,675 @@ AS $function$
)
))
$function$;
-- Прежние 6-аргументные сигнатуры заменяются: тип результата меняется, перегрузка не нужна.
DROP FUNCTION IF EXISTS v3.v_svod2_go(integer, integer[], integer, integer, varchar, varchar);
DROP FUNCTION IF EXISTS v3.v_svod2_rf(integer, integer[], integer, integer, varchar, varchar);
-- Общий отбор строк свода 2 для страниц и вариантов фильтров. Условия отбора как в 0049,
-- форма выбирается по p_sheet; p_search (уже нормализованный) отсекает строки без совпадения
-- по полям header, contract_summary и contract_detail, как matchesSearch экрана.
CREATE OR REPLACE FUNCTION v3.svod2_lines(
p_sheet varchar,
p_year integer,
p_org_unit_ids integer[],
p_search text
)
RETURNS TABLE(
line_id integer, form_type_code varchar, form_id integer, org_unit_id integer,
org_name varchar, section_code varchar, item_id varchar,
smeta_type varchar, smeta_direction varchar
)
LANGUAGE sql STABLE
AS $function$
WITH empty_sections AS MATERIALIZED (
SELECT
to_jsonb(jsonb_populate_record(NULL::v3.contract_summary, '{}'::jsonb)) - 'line_id' AS contract_summary,
to_jsonb(jsonb_populate_record(NULL::v3.contract_detail, '{}'::jsonb)) - 'line_id' AS contract_detail
), eligible_lines AS (
-- svod2_lines eligible: begin
SELECT DISTINCT
bl.id AS line_id,
bf.form_type_code,
bf.id AS form_id,
bf.org_unit_id,
ou.title AS org_name,
ei.section_code,
ei.item_id,
CASE bl.direction
WHEN 'Support' THEN 'Поддержка'
WHEN 'Development' THEN 'Развитие'
ELSE NULL
END::varchar AS smeta_direction,
CASE ei.sheet WHEN 'AHR' THEN 'АХР' WHEN 'CAP' THEN 'КВ'
WHEN 'OPER' THEN 'Операц' END::varchar AS smeta_type
FROM v3.budget_form bf
JOIN v3.budget_line bl ON bl.budget_form_id = bf.id
JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
LEFT JOIN v3.plan presence_plan ON presence_plan.line_id = bl.id
LEFT JOIN v3.reserve presence_reserve ON presence_reserve.line_id = bl.id
LEFT JOIN v3.contract_summary presence_contract_summary ON presence_contract_summary.line_id = bl.id
LEFT JOIN v3.allocation presence_allocation ON presence_allocation.line_id = bl.id
LEFT JOIN v3.collegial_approval presence_collegial_approval ON presence_collegial_approval.line_id = bl.id
LEFT JOIN v3.ckk presence_ckk ON presence_ckk.line_id = bl.id
LEFT JOIN v3.contract_detail presence_contract_detail ON presence_contract_detail.line_id = bl.id
LEFT JOIN v3.sequestration presence_sequestration ON presence_sequestration.line_id = bl.id
LEFT JOIN v3.booking presence_booking ON presence_booking.line_id = bl.id
LEFT JOIN v3.budget_line_quarter presence_budget_line_quarter ON presence_budget_line_quarter.line_id = bl.id
LEFT JOIN v3.rent_detail presence_rent_detail ON presence_rent_detail.line_id = bl.id
LEFT JOIN v3.security_detail presence_security_detail ON presence_security_detail.line_id = bl.id
LEFT JOIN v3.utility_detail presence_utility_detail ON presence_utility_detail.line_id = bl.id
WHERE bf.year = p_year
AND ( (p_sheet = 'GO' AND bf.form_type_code IN ('FORM_1', 'FORM_4'))
OR (p_sheet = 'RF' AND bf.form_type_code = 'FORM_2'))
AND ei.sheet IN ('AHR', 'CAP', 'OPER')
AND (p_org_unit_ids IS NULL OR cardinality(p_org_unit_ids) = 0
OR bf.org_unit_id = ANY(p_org_unit_ids))
AND (
(
presence_plan.plan_q1 <> 0
OR presence_plan.plan_q2 <> 0
OR presence_plan.plan_q3 <> 0
OR presence_plan.plan_q4 <> 0
OR presence_plan.comment ~ '[^[:space:]]'
)
OR presence_reserve.line_id IS NOT NULL
OR presence_contract_summary.line_id IS NOT NULL
OR presence_allocation.line_id IS NOT NULL
OR presence_collegial_approval.line_id IS NOT NULL
OR presence_ckk.line_id IS NOT NULL
OR presence_contract_detail.line_id IS NOT NULL
OR presence_sequestration.line_id IS NOT NULL
OR presence_booking.line_id IS NOT NULL
OR (
presence_budget_line_quarter.adj_current <> 0
OR presence_budget_line_quarter.adj_ssp <> 0
OR presence_budget_line_quarter.adj_rf <> 0
OR presence_budget_line_quarter.adj_reserve <> 0
OR presence_budget_line_quarter.adj_comment ~ '[^[:space:]]'
OR presence_budget_line_quarter.target_change <> 0
OR presence_budget_line_quarter.base_plan_correction <> 0
OR presence_budget_line_quarter.base_plan_correction_comment ~ '[^[:space:]]'
OR presence_budget_line_quarter.payment_date IS NOT NULL
OR presence_budget_line_quarter.payment_amount <> 0
OR presence_budget_line_quarter.payment_amount_ho <> 0
OR presence_budget_line_quarter.payment_amount_rf <> 0
OR presence_budget_line_quarter.payment_comment ~ '[^[:space:]]'
OR presence_budget_line_quarter.payment_act ~ '[^[:space:]]'
OR presence_budget_line_quarter.actual_m1 <> 0
OR presence_budget_line_quarter.actual_m2 <> 0
OR presence_budget_line_quarter.actual_m3 <> 0
OR presence_budget_line_quarter.actual_spod <> 0
OR presence_budget_line_quarter.transfer_to_q2 <> 0
OR presence_budget_line_quarter.transfer_to_q3 <> 0
OR presence_budget_line_quarter.transfer_to_q4 <> 0
OR presence_budget_line_quarter.transfer_to_economy <> 0
OR presence_budget_line_quarter.transfer_delay_acts <> 0
OR presence_budget_line_quarter.transfer_delay_procurement <> 0
OR presence_budget_line_quarter.transfer_economy_rf <> 0
OR presence_budget_line_quarter.transfer_next_comment ~ '[^[:space:]]'
OR presence_budget_line_quarter.transfer_far_comment ~ '[^[:space:]]'
OR presence_budget_line_quarter.booking_amount <> 0
OR presence_budget_line_quarter.plan_revision_eco_change <> 0
OR presence_budget_line_quarter.plan_revision_item_adj <> 0
OR presence_budget_line_quarter.plan_revision_increase <> 0
OR presence_budget_line_quarter.plan_revision_sequester <> 0
OR presence_budget_line_quarter.plan_revision_comment ~ '[^[:space:]]'
)
OR (
presence_rent_detail.contract_number ~ '[^[:space:]]'
OR presence_rent_detail.contract_end_date IS NOT NULL
OR presence_rent_detail.plan_q1 <> 0
OR presence_rent_detail.plan_q2 <> 0
OR presence_rent_detail.plan_q3 <> 0
OR presence_rent_detail.plan_q4 <> 0
OR presence_rent_detail.actual_jan <> 0
OR presence_rent_detail.actual_feb <> 0
OR presence_rent_detail.actual_mar <> 0
OR presence_rent_detail.actual_apr <> 0
OR presence_rent_detail.actual_may <> 0
OR presence_rent_detail.actual_jun <> 0
OR presence_rent_detail.actual_jul <> 0
OR presence_rent_detail.actual_aug <> 0
OR presence_rent_detail.actual_sep <> 0
OR presence_rent_detail.actual_oct <> 0
OR presence_rent_detail.actual_nov <> 0
OR presence_rent_detail.actual_dec <> 0
OR presence_rent_detail.comment ~ '[^[:space:]]'
)
OR presence_security_detail.line_id IS NOT NULL
OR presence_utility_detail.line_id IS NOT NULL
)
-- svod2_lines eligible: end
)
SELECT el.line_id, el.form_type_code, el.form_id, el.org_unit_id, el.org_name,
el.section_code, el.item_id, el.smeta_type, el.smeta_direction
FROM eligible_lines el
WHERE p_search IS NULL OR EXISTS (
SELECT 1
FROM v3.budget_line bl
JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
LEFT JOIN v3.contract_summary cs ON cs.line_id = bl.id
LEFT JOIN v3.contract_detail cd ON cd.line_id = bl.id
LEFT JOIN v3.ckk ck ON ck.line_id = bl.id
CROSS JOIN empty_sections e
CROSS JOIN LATERAL (
SELECT
jsonb_build_object(
'section_code', ei.section_code, 'item_id', ei.item_id,
'num_group', ei.num_group_id, 'name', COALESCE(bl.name, ei.name),
'expense_item_id', ei.id, 'justification', bl.justification,
'internal_order', bl.internal_order, 'vsp_id', bl.vsp_id,
'project_id', bl.project_id, 'direction', bl.direction,
'smeta_direction', CASE bl.direction
WHEN 'Support' THEN 'Поддержка'
WHEN 'Development' THEN 'Развитие'
ELSE NULL
END,
'source_form', el.form_type_code, 'source_id', el.form_id,
'source_org_unit_id', el.org_unit_id, 'org_name', el.org_name,
'smeta_type', el.smeta_type) AS header_j,
COALESCE(to_jsonb(cs) - 'line_id', e.contract_summary) AS cs_j,
CASE WHEN p_sheet = 'RF'
THEN COALESCE(to_jsonb(cd) - 'line_id', e.contract_detail) || jsonb_build_object(
'expenses_q1', COALESCE(cd.expenses_q1, ck.expenses_q1),
'expenses_q2', COALESCE(cd.expenses_q2, ck.expenses_q2),
'expenses_q3', COALESCE(cd.expenses_q3, ck.expenses_q3),
'expenses_q4', COALESCE(cd.expenses_q4, ck.expenses_q4))
ELSE COALESCE(to_jsonb(cd) - 'line_id', e.contract_detail)
END AS cd_j
) s
CROSS JOIN LATERAL jsonb_each(s.header_j || s.cs_j || s.cd_j) kv
WHERE bl.id = el.line_id
AND strpos(
v3.svod2_casefold(
CASE jsonb_typeof(kv.value)
WHEN 'string' THEN kv.value #>> '{}'
WHEN 'number' THEN v3.svod2_js_number_text((kv.value #>> '{}')::float8)
WHEN 'boolean' THEN kv.value #>> '{}'
WHEN 'null' THEN ''
ELSE kv.value::text
END),
p_search) > 0
)
$function$;
-- Страница свода 2 для ГО: поиск и три фильтра на сервере, total_count - число строк до LIMIT.
CREATE OR REPLACE FUNCTION v3.v_svod2_go(
p_year integer,
p_org_unit_ids integer[] DEFAULT NULL,
p_offset integer DEFAULT NULL,
p_limit integer DEFAULT NULL,
p_sort_by varchar DEFAULT 'org_name',
p_sort_direction varchar DEFAULT 'asc',
p_search text DEFAULT NULL,
p_smeta_type text DEFAULT NULL,
p_smeta_direction text DEFAULT NULL,
p_org_name text DEFAULT NULL,
p_compact boolean DEFAULT false
)
RETURNS TABLE(
source_form varchar, source_id integer, source_org_unit_id integer,
org_name varchar, smeta_type varchar,
source_line_id integer, sort_order bigint, data jsonb, total_count bigint
)
LANGUAGE sql STABLE SET search_path TO 'v3', 'pg_catalog'
AS $function$
WITH empty_sections AS MATERIALIZED (
SELECT
(to_jsonb(jsonb_populate_record(NULL::v3.plan, '{}'::jsonb)) - 'line_id')
|| jsonb_build_object('plan_year', NULL) AS plan,
(to_jsonb(jsonb_populate_record(NULL::v3.sequestration, '{}'::jsonb))
- 'id' - 'line_id' - 'actor') || jsonb_build_object('adj_year', NULL) AS sequestration,
(to_jsonb(jsonb_populate_record(NULL::v3.reserve, '{}'::jsonb)) - 'line_id')
|| jsonb_build_object('amount_year', NULL) AS reserve,
jsonb_build_object(
'approved_q1', NULL, 'approved_q2', NULL,
'approved_q3', NULL, 'approved_q4', NULL,
'approved_year', NULL
) AS approved,
to_jsonb(jsonb_populate_record(NULL::v3.contract_summary, '{}'::jsonb)) - 'line_id' AS contract_summary,
to_jsonb(jsonb_populate_record(NULL::v3.allocation, '{}'::jsonb)) - 'line_id' AS allocation,
to_jsonb(jsonb_populate_record(NULL::v3.collegial_approval, '{}'::jsonb)) - 'line_id' AS collegial,
to_jsonb(jsonb_populate_record(NULL::v3.ckk, '{}'::jsonb)) - 'line_id' AS ckk,
to_jsonb(jsonb_populate_record(NULL::v3.contract_detail, '{}'::jsonb)) - 'line_id' AS contract_detail,
to_jsonb(jsonb_populate_record(NULL::v3.booking, '{}'::jsonb)) - 'id' - 'line_id' AS booking,
to_jsonb(jsonb_populate_record(NULL::v3.budget_line_quarter, '{}'::jsonb))
- 'line_id' - 'quarter' AS quarter
), numbered_lines AS (
SELECT el.*,
count(*) OVER () AS total_count,
row_number() OVER (
ORDER BY
CASE WHEN p_sort_by = 'org_name' AND lower(p_sort_direction) = 'asc'
THEN el.org_name END ASC NULLS LAST,
CASE WHEN p_sort_by = 'org_name' AND lower(p_sort_direction) = 'desc'
THEN el.org_name END DESC NULLS LAST,
CASE WHEN p_sort_by = 'org_name' AND lower(p_sort_direction) = 'asc'
THEN el.item_id END ASC NULLS LAST,
CASE WHEN p_sort_by = 'org_name' AND lower(p_sort_direction) = 'desc'
THEN el.item_id END DESC NULLS LAST,
CASE WHEN p_sort_by = 'section_code' AND lower(p_sort_direction) = 'asc'
THEN el.section_code END ASC NULLS LAST,
CASE WHEN p_sort_by = 'section_code' AND lower(p_sort_direction) = 'desc'
THEN el.section_code END DESC NULLS LAST,
el.form_type_code, el.form_id, el.line_id
) AS sort_order
FROM v3.svod2_lines('GO', p_year, p_org_unit_ids,
NULLIF(v3.svod2_casefold(v3.svod2_js_trim(p_search)), '')) el
WHERE (NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_type)), '') IS NULL
OR (el.smeta_type IS NOT NULL
AND v3.svod2_js_trim(v3.svod2_casefold(el.smeta_type))
= NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_type)), '')))
AND (NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_direction)), '') IS NULL
OR (el.smeta_direction IS NOT NULL
AND v3.svod2_js_trim(v3.svod2_casefold(el.smeta_direction))
= NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_direction)), '')))
AND (NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_org_name)), '') IS NULL
OR (el.org_name IS NOT NULL
AND v3.svod2_js_trim(v3.svod2_casefold(el.org_name))
= NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_org_name)), '')))
), page AS MATERIALIZED (
SELECT * FROM numbered_lines
ORDER BY sort_order
LIMIT p_limit OFFSET COALESCE(p_offset, 0)
), booking_data AS (
SELECT b.line_id,
jsonb_agg(to_jsonb(b) - 'id' - 'line_id' ORDER BY b.quarter, b.source) AS booking
FROM page pg
JOIN v3.booking b ON b.line_id = pg.line_id
GROUP BY b.line_id
)
SELECT x.source_form, x.source_id, x.source_org_unit_id, x.org_name, x.smeta_type,
x.source_line_id, x.sort_order,
CASE WHEN p_compact THEN v3.svod2_compact(x.full_data) ELSE x.full_data END AS data,
x.total_count
FROM (
SELECT
pg.form_type_code AS source_form,
pg.form_id AS source_id,
pg.org_unit_id AS source_org_unit_id,
pg.org_name,
pg.smeta_type,
pg.line_id AS source_line_id,
pg.sort_order,
jsonb_build_object(
'line_id', bl.id,
'header', jsonb_build_object(
'section_code', ei.section_code, 'item_id', ei.item_id,
'num_group', ei.num_group_id, 'name', COALESCE(bl.name, ei.name),
'expense_item_id', ei.id, 'justification', bl.justification,
'internal_order', bl.internal_order, 'vsp_id', bl.vsp_id,
'project_id', bl.project_id, 'direction', bl.direction,
'smeta_direction', CASE bl.direction
WHEN 'Support' THEN 'Поддержка'
WHEN 'Development' THEN 'Развитие'
ELSE NULL
END,
'source_form', pg.form_type_code, 'source_id', pg.form_id,
'source_org_unit_id', pg.org_unit_id, 'org_name', pg.org_name,
'smeta_type', pg.smeta_type),
'plan', COALESCE(
(to_jsonb(pl) - 'line_id') || jsonb_build_object(
'plan_year', COALESCE(pl.plan_q1, 0) + COALESCE(pl.plan_q2, 0)
+ COALESCE(pl.plan_q3, 0) + COALESCE(pl.plan_q4, 0)),
e.plan || jsonb_build_object('plan_year', NULL)),
'sequestration', jsonb_build_object(
'DFIP', COALESCE(
(to_jsonb(sd) - 'id' - 'line_id' - 'actor') || jsonb_build_object(
'adj_year', COALESCE(sd.adj_q1, 0) + COALESCE(sd.adj_q2, 0)
+ COALESCE(sd.adj_q3, 0) + COALESCE(sd.adj_q4, 0)),
e.sequestration || jsonb_build_object('adj_year', NULL)),
'SSP_GO', COALESCE(
(to_jsonb(ss) - 'id' - 'line_id' - 'actor') || jsonb_build_object(
'adj_year', COALESCE(ss.adj_q1, 0) + COALESCE(ss.adj_q2, 0)
+ COALESCE(ss.adj_q3, 0) + COALESCE(ss.adj_q4, 0)),
e.sequestration || jsonb_build_object('adj_year', NULL))),
'reserve', COALESCE(
(to_jsonb(rv) - 'line_id') || jsonb_build_object(
'amount_year', COALESCE(rv.amount_q1, 0) + COALESCE(rv.amount_q2, 0)
+ COALESCE(rv.amount_q3, 0) + COALESCE(rv.amount_q4, 0)),
e.reserve || jsonb_build_object('amount_year', NULL)),
'approved', CASE WHEN pl.line_id IS NULL AND sd.id IS NULL
AND rv.line_id IS NULL
THEN e.approved
ELSE jsonb_build_object(
'approved_q1', COALESCE(pl.plan_q1, 0) + COALESCE(sd.adj_q1, 0)
+ COALESCE(rv.amount_q1, 0),
'approved_q2', COALESCE(pl.plan_q2, 0) + COALESCE(sd.adj_q2, 0)
+ COALESCE(rv.amount_q2, 0),
'approved_q3', COALESCE(pl.plan_q3, 0) + COALESCE(sd.adj_q3, 0)
+ COALESCE(rv.amount_q3, 0),
'approved_q4', COALESCE(pl.plan_q4, 0) + COALESCE(sd.adj_q4, 0)
+ COALESCE(rv.amount_q4, 0),
'approved_year', COALESCE(pl.plan_q1, 0) + COALESCE(pl.plan_q2, 0)
+ COALESCE(pl.plan_q3, 0) + COALESCE(pl.plan_q4, 0)
+ COALESCE(sd.adj_q1, 0) + COALESCE(sd.adj_q2, 0)
+ COALESCE(sd.adj_q3, 0) + COALESCE(sd.adj_q4, 0)
+ COALESCE(rv.amount_q1, 0) + COALESCE(rv.amount_q2, 0)
+ COALESCE(rv.amount_q3, 0) + COALESCE(rv.amount_q4, 0)) END,
'contract_summary', COALESCE(to_jsonb(cs) - 'line_id', e.contract_summary),
'allocation', COALESCE(to_jsonb(al) - 'line_id', e.allocation),
'collegial', COALESCE(to_jsonb(ca) - 'line_id', e.collegial),
'ckk', COALESCE(to_jsonb(ck) - 'line_id', e.ckk),
'contract_detail', COALESCE(to_jsonb(cd) - 'line_id', e.contract_detail),
'booking', COALESCE(bk.booking, jsonb_build_array(e.booking)),
'q1', COALESCE(to_jsonb(q1) - 'line_id' - 'quarter', e.quarter) || jsonb_build_object(
'booking_amount', CASE WHEN pg.form_type_code = 'FORM_1'
THEN CASE WHEN COALESCE(cd.expenses_q1, 0) > 0 THEN cd.expenses_q1 ELSE ck.expenses_q1 END
ELSE COALESCE(q1.booking_amount,
CASE WHEN ei.sheet = 'AHR' THEN ck.expenses_q1
WHEN ck.expenses_q1 > 0 THEN ck.expenses_q1
ELSE cd.expenses_q1 END) END),
'q2', COALESCE(to_jsonb(q2) - 'line_id' - 'quarter', e.quarter) || jsonb_build_object(
'booking_amount', CASE WHEN pg.form_type_code = 'FORM_1'
THEN CASE WHEN COALESCE(cd.expenses_q2, 0) > 0 THEN cd.expenses_q2 ELSE ck.expenses_q2 END
ELSE COALESCE(q2.booking_amount,
CASE WHEN ei.sheet = 'AHR' THEN ck.expenses_q2
WHEN ck.expenses_q2 > 0 THEN ck.expenses_q2
ELSE cd.expenses_q2 END) END,
'plan_revision_eco_change', COALESCE(q2.target_change, q2.plan_revision_eco_change),
'plan_revision_item_adj', COALESCE(q2.base_plan_correction, q2.plan_revision_item_adj)),
'q3', COALESCE(to_jsonb(q3) - 'line_id' - 'quarter', e.quarter) || jsonb_build_object(
'booking_amount', CASE WHEN pg.form_type_code = 'FORM_1'
THEN CASE WHEN COALESCE(cd.expenses_q3, 0) > 0 THEN cd.expenses_q3 ELSE ck.expenses_q3 END
ELSE COALESCE(q3.booking_amount,
CASE WHEN ei.sheet = 'AHR' THEN ck.expenses_q3
WHEN ck.expenses_q3 > 0 THEN ck.expenses_q3
ELSE cd.expenses_q3 END) END,
'plan_revision_eco_change', COALESCE(q3.target_change, q3.plan_revision_eco_change),
'plan_revision_item_adj', COALESCE(q3.base_plan_correction, q3.plan_revision_item_adj)),
'q4', COALESCE(to_jsonb(q4) - 'line_id' - 'quarter', e.quarter) || jsonb_build_object(
'booking_amount', CASE WHEN pg.form_type_code = 'FORM_1'
THEN CASE WHEN COALESCE(cd.expenses_q4, 0) > 0 THEN cd.expenses_q4 ELSE ck.expenses_q4 END
ELSE COALESCE(q4.booking_amount,
CASE WHEN ei.sheet = 'AHR' THEN ck.expenses_q4
WHEN ck.expenses_q4 > 0 THEN ck.expenses_q4
ELSE cd.expenses_q4 END) END,
'plan_revision_eco_change', COALESCE(q4.target_change, q4.plan_revision_eco_change),
'plan_revision_item_adj', COALESCE(q4.base_plan_correction, q4.plan_revision_item_adj))
) AS full_data,
pg.total_count
FROM page pg
JOIN v3.budget_line bl ON bl.id = pg.line_id
JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
LEFT JOIN v3.contract_detail cd ON cd.line_id = bl.id
CROSS JOIN empty_sections e
LEFT JOIN v3.plan pl ON pl.line_id = bl.id
LEFT JOIN v3.sequestration sd ON sd.line_id = bl.id AND sd.actor = 'DFIP'
LEFT JOIN v3.sequestration ss ON ss.line_id = bl.id AND ss.actor = 'SSP_GO'
LEFT JOIN v3.reserve rv ON rv.line_id = bl.id
LEFT JOIN v3.contract_summary cs ON cs.line_id = bl.id
LEFT JOIN v3.allocation al ON al.line_id = bl.id
LEFT JOIN v3.collegial_approval ca ON ca.line_id = bl.id
LEFT JOIN v3.ckk ck ON ck.line_id = bl.id
LEFT JOIN booking_data bk ON bk.line_id = bl.id
LEFT JOIN v3.budget_line_quarter q1 ON q1.line_id = bl.id AND q1.quarter = 1
LEFT JOIN v3.budget_line_quarter q2 ON q2.line_id = bl.id AND q2.quarter = 2
LEFT JOIN v3.budget_line_quarter q3 ON q3.line_id = bl.id AND q3.quarter = 3
LEFT JOIN v3.budget_line_quarter q4 ON q4.line_id = bl.id AND q4.quarter = 4
) x
ORDER BY x.sort_order;
$function$;
-- Страница свода 2 для РФ.
CREATE OR REPLACE FUNCTION v3.v_svod2_rf(
p_year integer,
p_org_unit_ids integer[] DEFAULT NULL,
p_offset integer DEFAULT NULL,
p_limit integer DEFAULT NULL,
p_sort_by varchar DEFAULT 'org_name',
p_sort_direction varchar DEFAULT 'asc',
p_search text DEFAULT NULL,
p_smeta_type text DEFAULT NULL,
p_smeta_direction text DEFAULT NULL,
p_org_name text DEFAULT NULL,
p_compact boolean DEFAULT false
)
RETURNS TABLE(
source_form varchar, source_id integer, source_org_unit_id integer,
org_name varchar, smeta_type varchar,
source_line_id integer, sort_order bigint, data jsonb, total_count bigint
)
LANGUAGE sql STABLE SET search_path TO 'v3', 'pg_catalog'
AS $function$
WITH empty_sections AS MATERIALIZED (
SELECT
(to_jsonb(jsonb_populate_record(NULL::v3.plan, '{}'::jsonb)) - 'line_id')
|| jsonb_build_object('plan_year', NULL) AS plan,
(to_jsonb(jsonb_populate_record(NULL::v3.sequestration, '{}'::jsonb))
- 'id' - 'line_id' - 'actor') || jsonb_build_object('adj_year', NULL) AS sequestration,
(to_jsonb(jsonb_populate_record(NULL::v3.reserve, '{}'::jsonb)) - 'line_id')
|| jsonb_build_object('amount_year', NULL) AS reserve,
jsonb_build_object(
'approved_q1', NULL, 'approved_q2', NULL,
'approved_q3', NULL, 'approved_q4', NULL,
'approved_year', NULL
) AS approved,
to_jsonb(jsonb_populate_record(NULL::v3.contract_summary, '{}'::jsonb)) - 'line_id' AS contract_summary,
to_jsonb(jsonb_populate_record(NULL::v3.allocation, '{}'::jsonb)) - 'line_id' AS allocation,
to_jsonb(jsonb_populate_record(NULL::v3.collegial_approval, '{}'::jsonb)) - 'line_id' AS collegial,
to_jsonb(jsonb_populate_record(NULL::v3.ckk, '{}'::jsonb)) - 'line_id' AS ckk,
to_jsonb(jsonb_populate_record(NULL::v3.contract_detail, '{}'::jsonb)) - 'line_id' AS contract_detail,
to_jsonb(jsonb_populate_record(NULL::v3.booking, '{}'::jsonb)) - 'id' - 'line_id' AS booking,
to_jsonb(jsonb_populate_record(NULL::v3.budget_line_quarter, '{}'::jsonb))
- 'line_id' - 'quarter' AS budget_quarter
), numbered_lines AS (
SELECT el.*,
count(*) OVER () AS total_count,
row_number() OVER (
ORDER BY
CASE WHEN p_sort_by = 'org_name' AND lower(p_sort_direction) = 'asc'
THEN el.org_name END ASC NULLS LAST,
CASE WHEN p_sort_by = 'org_name' AND lower(p_sort_direction) = 'desc'
THEN el.org_name END DESC NULLS LAST,
CASE WHEN p_sort_by = 'org_name' AND lower(p_sort_direction) = 'asc'
THEN el.item_id END ASC NULLS LAST,
CASE WHEN p_sort_by = 'org_name' AND lower(p_sort_direction) = 'desc'
THEN el.item_id END DESC NULLS LAST,
CASE WHEN p_sort_by = 'section_code' AND lower(p_sort_direction) = 'asc'
THEN el.section_code END ASC NULLS LAST,
CASE WHEN p_sort_by = 'section_code' AND lower(p_sort_direction) = 'desc'
THEN el.section_code END DESC NULLS LAST,
el.form_type_code, el.form_id, el.line_id
) AS sort_order
FROM v3.svod2_lines('RF', p_year, p_org_unit_ids,
NULLIF(v3.svod2_casefold(v3.svod2_js_trim(p_search)), '')) el
WHERE (NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_type)), '') IS NULL
OR (el.smeta_type IS NOT NULL
AND v3.svod2_js_trim(v3.svod2_casefold(el.smeta_type))
= NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_type)), '')))
AND (NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_direction)), '') IS NULL
OR (el.smeta_direction IS NOT NULL
AND v3.svod2_js_trim(v3.svod2_casefold(el.smeta_direction))
= NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_direction)), '')))
AND (NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_org_name)), '') IS NULL
OR (el.org_name IS NOT NULL
AND v3.svod2_js_trim(v3.svod2_casefold(el.org_name))
= NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_org_name)), '')))
), page AS MATERIALIZED (
SELECT * FROM numbered_lines
ORDER BY sort_order
LIMIT p_limit OFFSET COALESCE(p_offset, 0)
), booking_data AS (
SELECT b.line_id,
jsonb_agg(to_jsonb(b) - 'id' - 'line_id' ORDER BY b.quarter, b.source) AS booking
FROM page pg
JOIN v3.booking b ON b.line_id = pg.line_id
GROUP BY b.line_id
)
SELECT x.source_form, x.source_id, x.source_org_unit_id, x.org_name, x.smeta_type,
x.source_line_id, x.sort_order,
CASE WHEN p_compact THEN v3.svod2_compact(x.full_data) ELSE x.full_data END AS data,
x.total_count
FROM (
SELECT
pg.form_type_code AS source_form, pg.form_id AS source_id,
pg.org_unit_id AS source_org_unit_id, pg.org_name, pg.smeta_type,
pg.line_id AS source_line_id, pg.sort_order,
jsonb_build_object(
'line_id', bl.id,
'header', jsonb_build_object(
'section_code', ei.section_code, 'item_id', ei.item_id,
'num_group', ei.num_group_id, 'name', COALESCE(bl.name, ei.name),
'expense_item_id', ei.id, 'justification', bl.justification,
'internal_order', bl.internal_order, 'vsp_id', bl.vsp_id,
'project_id', bl.project_id, 'direction', bl.direction,
'smeta_direction', CASE bl.direction
WHEN 'Support' THEN 'Поддержка'
WHEN 'Development' THEN 'Развитие'
ELSE NULL
END,
'source_form', pg.form_type_code, 'source_id', pg.form_id,
'source_org_unit_id', pg.org_unit_id, 'org_name', pg.org_name,
'smeta_type', pg.smeta_type),
'plan', COALESCE(
(to_jsonb(pl) - 'line_id') || jsonb_build_object(
'plan_year', COALESCE(pl.plan_q1, 0) + COALESCE(pl.plan_q2, 0)
+ COALESCE(pl.plan_q3, 0) + COALESCE(pl.plan_q4, 0)),
e.plan || jsonb_build_object('plan_year', NULL)),
'sequestration', jsonb_build_object(
'DFIP', COALESCE(
(to_jsonb(sd) - 'id' - 'line_id' - 'actor') || jsonb_build_object(
'adj_year', COALESCE(sd.adj_q1, 0) + COALESCE(sd.adj_q2, 0)
+ COALESCE(sd.adj_q3, 0) + COALESCE(sd.adj_q4, 0)),
e.sequestration || jsonb_build_object('adj_year', NULL)),
'SSP_GO', COALESCE(
(to_jsonb(ss) - 'id' - 'line_id' - 'actor') || jsonb_build_object(
'adj_year', COALESCE(ss.adj_q1, 0) + COALESCE(ss.adj_q2, 0)
+ COALESCE(ss.adj_q3, 0) + COALESCE(ss.adj_q4, 0)),
e.sequestration || jsonb_build_object('adj_year', NULL))),
'reserve', COALESCE(
(to_jsonb(rv) - 'line_id') || jsonb_build_object(
'amount_year', COALESCE(rv.amount_q1, 0) + COALESCE(rv.amount_q2, 0)
+ COALESCE(rv.amount_q3, 0) + COALESCE(rv.amount_q4, 0)),
e.reserve || jsonb_build_object('amount_year', NULL)),
'approved', CASE WHEN pl.line_id IS NULL AND sd.id IS NULL
AND ss.id IS NULL AND rv.line_id IS NULL
THEN e.approved
ELSE jsonb_build_object(
'approved_q1', COALESCE(pl.plan_q1, 0) + COALESCE(sd.adj_q1, 0)
+ COALESCE(ss.adj_q1, 0) + COALESCE(rv.amount_q1, 0),
'approved_q2', COALESCE(pl.plan_q2, 0) + COALESCE(sd.adj_q2, 0)
+ COALESCE(ss.adj_q2, 0) + COALESCE(rv.amount_q2, 0),
'approved_q3', COALESCE(pl.plan_q3, 0) + COALESCE(sd.adj_q3, 0)
+ COALESCE(ss.adj_q3, 0) + COALESCE(rv.amount_q3, 0),
'approved_q4', COALESCE(pl.plan_q4, 0) + COALESCE(sd.adj_q4, 0)
+ COALESCE(ss.adj_q4, 0) + COALESCE(rv.amount_q4, 0),
'approved_year', COALESCE(pl.plan_q1, 0) + COALESCE(pl.plan_q2, 0)
+ COALESCE(pl.plan_q3, 0) + COALESCE(pl.plan_q4, 0)
+ COALESCE(sd.adj_q1, 0) + COALESCE(sd.adj_q2, 0)
+ COALESCE(sd.adj_q3, 0) + COALESCE(sd.adj_q4, 0)
+ COALESCE(ss.adj_q1, 0) + COALESCE(ss.adj_q2, 0)
+ COALESCE(ss.adj_q3, 0) + COALESCE(ss.adj_q4, 0)
+ COALESCE(rv.amount_q1, 0) + COALESCE(rv.amount_q2, 0)
+ COALESCE(rv.amount_q3, 0) + COALESCE(rv.amount_q4, 0)) END,
'contract_summary', COALESCE(to_jsonb(cs) - 'line_id', e.contract_summary),
'allocation', COALESCE(to_jsonb(al) - 'line_id', e.allocation),
'collegial', COALESCE(to_jsonb(ca) - 'line_id', e.collegial),
'ckk', COALESCE(to_jsonb(ck) - 'line_id', e.ckk),
'contract_detail', COALESCE(to_jsonb(cd) - 'line_id', e.contract_detail) || jsonb_build_object(
'expenses_q1', COALESCE(cd.expenses_q1, ck.expenses_q1),
'expenses_q2', COALESCE(cd.expenses_q2, ck.expenses_q2),
'expenses_q3', COALESCE(cd.expenses_q3, ck.expenses_q3),
'expenses_q4', COALESCE(cd.expenses_q4, ck.expenses_q4)),
'booking', COALESCE(bk.booking, jsonb_build_array(e.booking)),
'q1', COALESCE(to_jsonb(q1) - 'line_id' - 'quarter', e.budget_quarter) || jsonb_build_object(
'booking_amount', COALESCE(q1.booking_amount,
CASE WHEN ei.sheet = 'AHR' THEN ck.expenses_q1
WHEN ck.expenses_q1 > 0 THEN ck.expenses_q1
ELSE cd.expenses_q1 END)),
'q2', COALESCE(to_jsonb(q2) - 'line_id' - 'quarter', e.budget_quarter) || jsonb_build_object(
'booking_amount', COALESCE(q2.booking_amount,
CASE WHEN ei.sheet = 'AHR' THEN ck.expenses_q2
WHEN ck.expenses_q2 > 0 THEN ck.expenses_q2
ELSE cd.expenses_q2 END),
'plan_revision_eco_change', COALESCE(q2.target_change, q2.plan_revision_eco_change),
'plan_revision_item_adj', COALESCE(q2.base_plan_correction, q2.plan_revision_item_adj)),
'q3', COALESCE(to_jsonb(q3) - 'line_id' - 'quarter', e.budget_quarter) || jsonb_build_object(
'booking_amount', COALESCE(q3.booking_amount,
CASE WHEN ei.sheet = 'AHR' THEN ck.expenses_q3
WHEN ck.expenses_q3 > 0 THEN ck.expenses_q3
ELSE cd.expenses_q3 END),
'plan_revision_eco_change', COALESCE(q3.target_change, q3.plan_revision_eco_change),
'plan_revision_item_adj', COALESCE(q3.base_plan_correction, q3.plan_revision_item_adj)),
'q4', COALESCE(to_jsonb(q4) - 'line_id' - 'quarter', e.budget_quarter) || jsonb_build_object(
'booking_amount', COALESCE(q4.booking_amount,
CASE WHEN ei.sheet = 'AHR' THEN ck.expenses_q4
WHEN ck.expenses_q4 > 0 THEN ck.expenses_q4
ELSE cd.expenses_q4 END),
'plan_revision_eco_change', COALESCE(q4.target_change, q4.plan_revision_eco_change),
'plan_revision_item_adj', COALESCE(q4.base_plan_correction, q4.plan_revision_item_adj))
) AS full_data,
pg.total_count
FROM page pg
JOIN v3.budget_line bl ON bl.id = pg.line_id
JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
LEFT JOIN v3.contract_detail cd ON cd.line_id = bl.id
CROSS JOIN empty_sections e
LEFT JOIN v3.plan pl ON pl.line_id = bl.id
LEFT JOIN v3.sequestration sd ON sd.line_id = bl.id AND sd.actor = 'DFIP'
LEFT JOIN v3.sequestration ss ON ss.line_id = bl.id AND ss.actor = 'SSP_GO'
LEFT JOIN v3.reserve rv ON rv.line_id = bl.id
LEFT JOIN v3.contract_summary cs ON cs.line_id = bl.id
LEFT JOIN v3.allocation al ON al.line_id = bl.id
LEFT JOIN v3.collegial_approval ca ON ca.line_id = bl.id
LEFT JOIN v3.ckk ck ON ck.line_id = bl.id
LEFT JOIN booking_data bk ON bk.line_id = bl.id
LEFT JOIN v3.budget_line_quarter q1 ON q1.line_id = bl.id AND q1.quarter = 1
LEFT JOIN v3.budget_line_quarter q2 ON q2.line_id = bl.id AND q2.quarter = 2
LEFT JOIN v3.budget_line_quarter q3 ON q3.line_id = bl.id AND q3.quarter = 3
LEFT JOIN v3.budget_line_quarter q4 ON q4.line_id = bl.id AND q4.quarter = 4
) x
ORDER BY x.sort_order;
$function$;
-- Варианты фильтров и счётчики: как TanStack, с поиском и фильтрами двух других колонок.
CREATE OR REPLACE FUNCTION v3.v_svod2_facets(
p_sheet varchar,
p_year integer,
p_org_unit_ids integer[],
p_search text,
p_smeta_type text,
p_smeta_direction text,
p_org_name text
)
RETURNS TABLE(kind varchar, value varchar, n bigint)
LANGUAGE sql STABLE
AS $function$
WITH q AS MATERIALIZED (
SELECT NULLIF(v3.svod2_casefold(v3.svod2_js_trim(p_search)), '') AS s,
NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_type)), '') AS f_type,
NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_smeta_direction)), '') AS f_dir,
NULLIF(v3.svod2_js_trim(v3.svod2_casefold(p_org_name)), '') AS f_org
), lines AS MATERIALIZED (
SELECT l.smeta_type, l.smeta_direction, l.org_name,
(q.s IS NULL OR l.line_id IN (
SELECT m.line_id FROM v3.svod2_lines(p_sheet, p_year, p_org_unit_ids, q.s) m)) AS hit,
(q.f_type IS NULL OR (l.smeta_type IS NOT NULL
AND v3.svod2_js_trim(v3.svod2_casefold(l.smeta_type)) = q.f_type)) AS ok_type,
(q.f_dir IS NULL OR (l.smeta_direction IS NOT NULL
AND v3.svod2_js_trim(v3.svod2_casefold(l.smeta_direction)) = q.f_dir)) AS ok_dir,
(q.f_org IS NULL OR (l.org_name IS NOT NULL
AND v3.svod2_js_trim(v3.svod2_casefold(l.org_name)) = q.f_org)) AS ok_org
FROM v3.svod2_lines(p_sheet, p_year, p_org_unit_ids, NULL) l
CROSS JOIN q
)
SELECT 'total'::varchar, NULL::varchar, count(*)::bigint FROM lines
UNION ALL
SELECT 'search_total'::varchar, NULL::varchar, count(*)::bigint FROM lines WHERE hit
UNION ALL
SELECT 'smeta_type'::varchar, g.smeta_type::varchar, count(*)::bigint
FROM lines g WHERE g.hit AND g.ok_dir AND g.ok_org AND g.smeta_type IS NOT NULL
GROUP BY g.smeta_type
UNION ALL
SELECT 'smeta_direction'::varchar, g.smeta_direction::varchar, count(*)::bigint
FROM lines g WHERE g.hit AND g.ok_type AND g.ok_org AND g.smeta_direction IS NOT NULL
GROUP BY g.smeta_direction
UNION ALL
SELECT 'org_name'::varchar, g.org_name::varchar, count(*)::bigint
FROM lines g WHERE g.hit AND g.ok_type AND g.ok_dir AND g.org_name IS NOT NULL
GROUP BY g.org_name
$function$;

View File

@ -51,3 +51,70 @@ def test_compact_matches_python_projection(svod2_sql, svod2_data, client, admin_
full = legacy_full(client, "RF", auth_headers(admin_tokens))[0]["data"]
got = svod2_sql("SELECT v3.svod2_compact(CAST(:d AS jsonb)) AS c", {"d": json.dumps(full)})[0][0]
assert got == compact_projection(full)
def test_single_signature_without_overloads(svod2_sql):
rows = svod2_sql(
"SELECT proname, pronargs FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace "
"WHERE nspname = 'v3' AND proname IN ('v_svod2_go', 'v_svod2_rf', 'v_svod2_go_export')",
None,
)
assert sorted((r.proname, r.pronargs) for r in rows) == [
("v_svod2_go", 11), ("v_svod2_go_export", 4), ("v_svod2_rf", 11)]
@pytest.mark.parametrize("fn", ["v_svod2_go", "v_svod2_rf"])
def test_page_total_count_and_order(svod2_sql, svod2_data, admin_tokens, fn):
# admin_tokens: запрос к API нужен, чтобы откат сессии клиента сработал (loop_ref задаётся при обращении к get_db)
full = svod2_sql(f"SELECT source_line_id FROM v3.{fn}(2031)", None)
page = svod2_sql(
f"SELECT source_line_id, total_count FROM v3.{fn}(2031, NULL, 0, 2, 'org_name', 'asc', "
"NULL, NULL, NULL, NULL, false)", None)
assert [r.source_line_id for r in page] == [r.source_line_id for r in full][:2]
assert {r.total_count for r in page} == {len(full)}
def test_facets_totals(svod2_sql, svod2_data, admin_tokens):
rows = {(r.kind, r.value): r.n for r in svod2_sql(
"SELECT * FROM v3.v_svod2_facets('GO', 2031, NULL, NULL, NULL, NULL, NULL)", None)}
assert rows[("total", None)] == rows[("search_total", None)] == len(
svod2_sql("SELECT 1 FROM v3.v_svod2_go(2031)", None))
SEARCHES = ["", "аренда", "АРЕНДА", "ё", "1234.5", "1234.50", " кв ", "1", "true", "777.25", "нет такого"]
@pytest.mark.parametrize("sheet,fn", [("GO", "v_svod2_go"), ("RF", "v_svod2_rf")])
@pytest.mark.parametrize("search", SEARCHES)
def test_search_matches_screen_oracle(svod2_sql, svod2_data, client, admin_tokens, auth_headers,
sheet, fn, search):
from tests.svod2_oracle import screen_rows
rows = legacy_full(client, sheet, auth_headers(admin_tokens))
expected = [r["data"]["line_id"] for r in screen_rows(rows, search)]
got = svod2_sql(
f"SELECT source_line_id, total_count FROM v3.{fn}(2031, NULL, NULL, NULL, 'org_name', 'asc', :s)",
{"s": search or None},
)
assert sorted(r.source_line_id for r in got) == sorted(expected)
if got:
assert {r.total_count for r in got} == {len(expected)}
@pytest.mark.parametrize("sheet", ["GO", "RF"])
@pytest.mark.parametrize("search,smeta_type", [("", None), ("1", "кв "), ("аренда", "АХР"), ("", "Операц")])
def test_facets_match_screen_oracle(svod2_sql, svod2_data, client, admin_tokens, auth_headers,
sheet, search, smeta_type):
from tests.svod2_oracle import screen_facets
rows = legacy_full(client, sheet, auth_headers(admin_tokens))
filters = {"smeta_type": smeta_type} if smeta_type else {}
expected = screen_facets(rows, search, filters)
got = svod2_sql(
"SELECT kind, value, n FROM v3.v_svod2_facets(:sh, 2031, NULL, :s, :t, NULL, NULL)",
{"sh": sheet, "s": search or None, "t": smeta_type},
)
assert next(r.n for r in got if r.kind == "total") == expected["total"]
assert next(r.n for r in got if r.kind == "search_total") == expected["search_total"]
for col in ("smeta_type", "smeta_direction", "org_name"):
assert {r.value: r.n for r in got if r.kind == col} == expected[col]

View File

@ -6,7 +6,9 @@ import unittest
from pathlib import Path
SQL_PATH = Path(__file__).resolve().parents[2] / "alembic/versions/sql/0049_summary2_related_records.sql"
SQL_DIR = Path(__file__).resolve().parents[2] / "alembic/versions/sql"
SQL_PATH = SQL_DIR / "0053_svod2_server_paging.sql"
EXPORT_SQL_PATH = SQL_DIR / "0049_summary2_related_records.sql"
TABLES = (
"plan", "reserve", "contract_summary", "allocation", "collegial_approval",
"ckk", "contract_detail", "sequestration", "booking", "budget_line_quarter",
@ -59,8 +61,14 @@ class Summary2RelatedRecordsSQLTest(unittest.TestCase):
self.db.execute(f"CREATE TABLE v3.{table} ({columns})")
# Execute the eligibility SELECTs, adapting PostgreSQL casts, arrays and regex syntax.
self.queries = []
for part in SQL_PATH.read_text().split("eligible_lines AS (")[1:]:
query = part.split("), ", 1)[0].replace("::varchar", "")
text = SQL_PATH.read_text()
match = re.search(r"-- svod2_lines eligible: begin\n(.*?)\n\s*-- svod2_lines eligible: end", text, re.S)
self.assertIsNotNone(match)
bodies = [match.group(1).replace("p_sheet", f"'{sheet}'") for sheet in ("GO", "RF")]
# Экспорт ГО по-прежнему из 0049: его отбор эта миграция не меняет.
export = EXPORT_SQL_PATH.read_text().split("eligible_lines AS (")[-1].split("), ", 1)[0]
for query in bodies + [export]:
query = query.replace("::varchar", "")
query = re.sub(r"(presence_\w+\.\w+) ~ '\[\^\[:space:\]\]'",
r"has_text(\1)", query)
query = query.replace("p_year", ":year")