svod-fix: свод 2 из формы 2, новый свод 1 для формы 3 #180

Merged
tsygankoviva merged 1 commits from svod-fix into test 2026-09-28 17:07:42 +03:00
3 changed files with 356 additions and 1 deletions

View File

@ -0,0 +1,15 @@
from sql_migration import run_sql_file_migration
revision = "0041"
down_revision = "0040"
branch_labels = None
depends_on = None
def upgrade() -> None:
run_sql_file_migration("0041_svod2_rf_form2_only.sql")
def downgrade() -> None:
pass

View File

@ -0,0 +1,299 @@
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'
)
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
)
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
), eligible_lines AS (
SELECT
'FORM_2'::varchar AS form_type_code,
bl.id AS line_id,
bf.id AS source_id,
bf.org_unit_id,
ou.title AS org_name,
ei.section_code,
ei.item_id,
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
JOIN v3.contract_detail cd ON cd.line_id = bl.id
LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
WHERE bf.year = p_year 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))
), numbered_lines AS (
SELECT el.*,
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.source_id, el.line_id
) AS sort_order
FROM eligible_lines el
), 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
pg.form_type_code AS source_form, pg.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.source_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),
'booking', COALESCE(bk.booking, jsonb_build_array(e.booking)),
'q1', COALESCE(to_jsonb(q1) - 'line_id' - 'quarter', e.budget_quarter),
'q2', COALESCE(to_jsonb(q2) - 'line_id' - 'quarter', e.budget_quarter),
'q3', COALESCE(to_jsonb(q3) - 'line_id' - 'quarter', e.budget_quarter),
'q4', COALESCE(to_jsonb(q4) - 'line_id' - 'quarter', e.budget_quarter)
) AS data
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
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
ORDER BY pg.sort_order;
$function$;
CREATE OR REPLACE FUNCTION v3.v_3form_rf_r(p_year integer, p_org_unit_ids integer[] DEFAULT NULL)
RETURNS TABLE(
section_code character varying,
name character varying,
plan_development_q1 numeric,
plan_development_q2 numeric,
plan_development_q3 numeric,
plan_development_q4 numeric,
plan_development_year numeric,
approved_development_q1 numeric,
approved_development_q2 numeric,
approved_development_q3 numeric,
approved_development_q4 numeric,
approved_development_year numeric,
fact_development_q1 numeric,
fact_development_q2 numeric,
fact_development_q3 numeric,
fact_development_q4 numeric,
fact_development_year numeric,
corrected_development_q2 numeric,
corrected_development_q3 numeric,
corrected_development_q4 numeric
)
LANGUAGE sql
STABLE
SET search_path TO 'v3', 'pg_catalog'
AS $function$
WITH
report_lines AS (
SELECT
l.expense_item_id AS eid,
q.quarter,
q.base_plan,
q.actual_m1,
q.actual_m2,
q.actual_m3,
q.actual_spod,
q.corrected_plan
FROM v3.rf_project_report AS r
JOIN v3.rf_project_year AS rpry ON rpry.id = r.rf_project_year_id
JOIN v3.project AS p ON p.id = r.project_id
JOIN v3.rf_project_report_line AS l ON l.rf_project_report_id = r.id
JOIN v3.rf_project_report_quarter AS q ON q.rf_project_report_line_id = l.id
WHERE rpry.year = p_year
AND (p_org_unit_ids IS NULL OR cardinality(p_org_unit_ids) = 0 OR p.org_unit_id = ANY(p_org_unit_ids))
),
agg AS (
SELECT
eid,
SUM(CASE WHEN quarter = 1 THEN base_plan END) AS bp1,
SUM(CASE WHEN quarter = 2 THEN base_plan END) AS bp2,
SUM(CASE WHEN quarter = 3 THEN base_plan END) AS bp3,
SUM(CASE WHEN quarter = 4 THEN base_plan END) AS bp4,
SUM(CASE WHEN quarter = 1 THEN actual_m1 END) AS m1_1,
SUM(CASE WHEN quarter = 2 THEN actual_m1 END) AS m1_2,
SUM(CASE WHEN quarter = 3 THEN actual_m1 END) AS m1_3,
SUM(CASE WHEN quarter = 4 THEN actual_m1 END) AS m1_4,
SUM(CASE WHEN quarter = 1 THEN actual_m2 END) AS m2_1,
SUM(CASE WHEN quarter = 2 THEN actual_m2 END) AS m2_2,
SUM(CASE WHEN quarter = 3 THEN actual_m2 END) AS m2_3,
SUM(CASE WHEN quarter = 4 THEN actual_m2 END) AS m2_4,
SUM(CASE WHEN quarter = 1 THEN actual_m3 END) AS m3_1,
SUM(CASE WHEN quarter = 2 THEN actual_m3 END) AS m3_2,
SUM(CASE WHEN quarter = 3 THEN actual_m3 END) AS m3_3,
SUM(CASE WHEN quarter = 4 THEN actual_m3 END) AS m3_4,
SUM(CASE WHEN quarter = 4 THEN actual_spod END) AS spod4,
SUM(CASE WHEN quarter = 2 THEN corrected_plan END) AS cp2,
SUM(CASE WHEN quarter = 3 THEN corrected_plan END) AS cp3,
SUM(CASE WHEN quarter = 4 THEN corrected_plan END) AS cp4
FROM report_lines
GROUP BY eid
),
tree_nodes AS (
SELECT t.id, t.item_id, t.name, t.desc_ids
FROM v3.mv_expense_item_tree AS t
JOIN v3.expense_item_form_type AS eft
ON eft.expense_item_id = t.id
AND eft.form_type_code = 'FORM_3'
WHERE t.depth < 3
),
rolled AS (
SELECT
tn.item_id,
tn.name,
SUM(a.bp1) AS bp1, SUM(a.bp2) AS bp2, SUM(a.bp3) AS bp3, SUM(a.bp4) AS bp4,
SUM(a.m1_1) AS m1_1, SUM(a.m1_2) AS m1_2, SUM(a.m1_3) AS m1_3, SUM(a.m1_4) AS m1_4,
SUM(a.m2_1) AS m2_1, SUM(a.m2_2) AS m2_2, SUM(a.m2_3) AS m2_3, SUM(a.m2_4) AS m2_4,
SUM(a.m3_1) AS m3_1, SUM(a.m3_2) AS m3_2, SUM(a.m3_3) AS m3_3, SUM(a.m3_4) AS m3_4,
SUM(a.spod4) AS spod4,
SUM(a.cp2) AS cp2, SUM(a.cp3) AS cp3, SUM(a.cp4) AS cp4
FROM tree_nodes AS tn
LEFT JOIN agg AS a ON a.eid = ANY (tn.desc_ids)
GROUP BY tn.item_id, tn.name
)
SELECT
r.item_id AS section_code,
r.name,
COALESCE(r.bp1, 0) AS plan_development_q1,
COALESCE(r.bp2, 0) AS plan_development_q2,
COALESCE(r.bp3, 0) AS plan_development_q3,
COALESCE(r.bp4, 0) AS plan_development_q4,
COALESCE(r.bp1, 0) + COALESCE(r.bp2, 0) + COALESCE(r.bp3, 0) + COALESCE(r.bp4, 0) AS plan_development_year,
COALESCE(r.bp1, 0) AS approved_development_q1,
COALESCE(r.bp2, 0) AS approved_development_q2,
COALESCE(r.bp3, 0) AS approved_development_q3,
COALESCE(r.bp4, 0) AS approved_development_q4,
COALESCE(r.bp1, 0) + COALESCE(r.bp2, 0) + COALESCE(r.bp3, 0) + COALESCE(r.bp4, 0) AS approved_development_year,
COALESCE(r.m1_1, 0) + COALESCE(r.m2_1, 0) + COALESCE(r.m3_1, 0) AS fact_development_q1,
COALESCE(r.m1_2, 0) + COALESCE(r.m2_2, 0) + COALESCE(r.m3_2, 0) AS fact_development_q2,
COALESCE(r.m1_3, 0) + COALESCE(r.m2_3, 0) + COALESCE(r.m3_3, 0) AS fact_development_q3,
COALESCE(r.m1_4, 0) + COALESCE(r.m2_4, 0) + COALESCE(r.m3_4, 0) + COALESCE(r.spod4, 0) AS fact_development_q4,
COALESCE(r.m1_1, 0) + COALESCE(r.m2_1, 0) + COALESCE(r.m3_1, 0)
+ COALESCE(r.m1_2, 0) + COALESCE(r.m2_2, 0) + COALESCE(r.m3_2, 0)
+ COALESCE(r.m1_3, 0) + COALESCE(r.m2_3, 0) + COALESCE(r.m3_3, 0)
+ COALESCE(r.m1_4, 0) + COALESCE(r.m2_4, 0) + COALESCE(r.m3_4, 0) + COALESCE(r.spod4, 0) AS fact_development_year,
COALESCE(r.cp2, 0) AS corrected_development_q2,
COALESCE(r.cp3, 0) AS corrected_development_q3,
COALESCE(r.cp4, 0) AS corrected_development_q4
FROM rolled AS r;
$function$;

View File

@ -23,6 +23,12 @@ const METRICS = [
{ key: 'fact', label: 'Факт' },
{ key: 'corrected', label: 'Скорректированный план' },
];
const FORM_3_METRIC_LABELS = {
plan: 'Расходы, планируемые РФ',
approved: 'Утвержденные базовые расходы',
fact: 'Фактические расходы',
corrected: 'Скорректированная смета расходов',
};
const DIRECTIONS = [
{ key: 'support', label: 'Поддержка' },
{ key: 'development', label: 'Развитие' },
@ -34,6 +40,13 @@ const QUARTERS = [
{ key: 'q4', label: 'IV кв.' },
{ key: 'year', label: 'Год' },
];
const FORM_3_QUARTERS = [
{ key: 'q1', label: 'I квартал' },
{ key: 'q2', label: 'II квартал' },
{ key: 'q3', label: 'III квартал' },
{ key: 'q4', label: 'IV квартал' },
{ key: 'year', label: 'ИТОГО Развитие' },
];
const numberFormatter = new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 2 });
const valueAt = (row, path) => path.reduce((value, key) => value?.[key], row.data);
const tableSize = (rem) => rem * 16;
@ -66,14 +79,42 @@ const valueColumn = (metric, direction, period) => ({
muiTableHeadCellProps: { align: 'right' },
});
const createForm3MetricColumns = (metric) => ({
id: metric.key,
header: FORM_3_METRIC_LABELS[metric.key],
columns: FORM_3_QUARTERS.filter(({ key }) =>
valueAt({ data: getShapeForSheet('FORM_3') }, [metric.key, 'development', key]) !== undefined,
).map((period) => ({
...valueColumn(metric, { key: 'development' }, period),
id: metric.key + '.' + period.key,
})),
});
const createColumns = (sheet) => {
let visibleDirections = DIRECTIONS;
if (sheet === 'FORM_2') visibleDirections = DIRECTIONS.filter(({ key }) => key === 'support');
if (['FORM_3', 'FORM_4'].includes(sheet)) visibleDirections = DIRECTIONS.filter(({ key }) => key === 'development');
const shape = getShapeForSheet(sheet);
if (sheet === 'FORM_3') {
return [
{ id: 'section_code', header: '№ п/п', accessorFn: (row) => row.data?.section_code, size: tableSize(5.75) },
{
id: 'name',
header: 'Перечень расходов и затрат',
accessorFn: (row) => row.data?.name,
size: tableSize(20.625),
Cell: ({ cell, row }) => (
<Box sx={{ pl: row.original.depth * 1.125 + 'rem', fontWeight: row.original.depth < 2 ? 600 : 400 }}>
{cell.getValue() || 'Без названия'}
</Box>
),
},
...METRICS.map(createForm3MetricColumns),
];
}
const metricColumns = METRICS.map((metric) => ({
id: metric.key,
header: metric.label,
header: sheet === 'FORM_3' ? FORM_3_METRIC_LABELS[metric.key] : metric.label,
columns: visibleDirections
.map((direction) => ({
id: `${metric.key}.${direction.key}`,