Merge pull request 'svod: свод 2' (#128) from svod into test
Reviewed-on: #128
This commit is contained in:
commit
02b746c5ad
61
api/alembic/versions/0022_summary2_contract_rows.py
Normal file
61
api/alembic/versions/0022_summary2_contract_rows.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
revision = "0022"
|
||||||
|
down_revision = "0021"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
_DOLLAR_TAG_RE = re.compile(r"\$\w+\$")
|
||||||
|
|
||||||
|
|
||||||
|
def _split_statements(sql: str) -> list[str]:
|
||||||
|
statements: list[str] = []
|
||||||
|
current: list[str] = []
|
||||||
|
dollar_tag: str | None = None
|
||||||
|
|
||||||
|
for line in sql.splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith("--"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if dollar_tag is None:
|
||||||
|
match = _DOLLAR_TAG_RE.search(stripped)
|
||||||
|
if match:
|
||||||
|
dollar_tag = match.group(0)
|
||||||
|
elif stripped.startswith(dollar_tag):
|
||||||
|
dollar_tag = None
|
||||||
|
|
||||||
|
current.append(line)
|
||||||
|
if dollar_tag is None and stripped.endswith(";"):
|
||||||
|
statements.append("\n".join(current))
|
||||||
|
current = []
|
||||||
|
|
||||||
|
if remaining := "\n".join(current).strip():
|
||||||
|
statements.append(remaining)
|
||||||
|
return statements
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
ddl_path = os.path.join(
|
||||||
|
os.path.dirname(__file__), "sql", "0022_summary2_contract_rows.sql"
|
||||||
|
)
|
||||||
|
with open(ddl_path) as ddl_file:
|
||||||
|
statements = _split_statements(ddl_file.read())
|
||||||
|
for statement in statements:
|
||||||
|
op.execute(statement.rstrip(";").strip())
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.execute("DROP FUNCTION IF EXISTS v3.v_svod2_rf(integer, integer[], integer, integer, varchar, varchar)")
|
||||||
|
op.execute("DROP FUNCTION IF EXISTS v3.v_svod2_go(integer, integer[], integer, integer, varchar, varchar)")
|
||||||
|
op.execute("DROP FUNCTION IF EXISTS v3.v_svod2_rf(integer, integer[], integer, integer)")
|
||||||
|
op.execute("DROP FUNCTION IF EXISTS v3.v_svod2_go(integer, integer[], integer, integer)")
|
||||||
|
op.execute("DROP FUNCTION IF EXISTS v3.v_svod2_rf(integer, integer[])")
|
||||||
|
op.execute("DROP FUNCTION IF EXISTS v3.v_svod2_go(integer, integer[])")
|
||||||
|
op.execute("DROP INDEX IF EXISTS v3.ix_v3_budget_line_svod2")
|
||||||
|
op.execute("DROP INDEX IF EXISTS v3.ix_v3_budget_form_svod2")
|
||||||
@ -1238,50 +1238,10 @@ FROM rolled AS r;
|
|||||||
|
|
||||||
DROP FUNCTION IF EXISTS v3.v_main_format(integer);
|
DROP FUNCTION IF EXISTS v3.v_main_format(integer);
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION v3.v_main_format(p_year integer, p_org_unit_ids integer[] DEFAULT NULL)
|
-- DROP FUNCTION v3.v_main_format(int4, _int4);
|
||||||
RETURNS TABLE(
|
|
||||||
section_code character varying,
|
CREATE OR REPLACE FUNCTION v3.v_main_format(p_year integer, p_org_unit_ids integer[] DEFAULT NULL::integer[])
|
||||||
name character varying,
|
RETURNS TABLE(section_code character varying, name character varying, plan_support_q1 numeric, plan_support_q2 numeric, plan_support_q3 numeric, plan_support_q4 numeric, plan_support_year numeric, plan_development_q1 numeric, plan_development_q2 numeric, plan_development_q3 numeric, plan_development_q4 numeric, plan_development_year numeric, plan_total_year numeric, approved_support_q1 numeric, approved_support_q2 numeric, approved_support_q3 numeric, approved_support_q4 numeric, approved_support_year numeric, approved_development_q1 numeric, approved_development_q2 numeric, approved_development_q3 numeric, approved_development_q4 numeric, approved_development_year numeric, approved_total_year numeric, fact_support_q1 numeric, fact_support_q2 numeric, fact_support_q3 numeric, fact_support_q4 numeric, fact_support_year numeric, fact_development_q1 numeric, fact_development_q2 numeric, fact_development_q3 numeric, fact_development_q4 numeric, fact_development_year numeric, fact_total_year numeric, corrected_support_q2 numeric, corrected_support_q3 numeric, corrected_support_q4 numeric, corrected_development_q2 numeric, corrected_development_q3 numeric, corrected_development_q4 numeric)
|
||||||
plan_support_q1 numeric,
|
|
||||||
plan_support_q2 numeric,
|
|
||||||
plan_support_q3 numeric,
|
|
||||||
plan_support_q4 numeric,
|
|
||||||
plan_support_year numeric,
|
|
||||||
plan_development_q1 numeric,
|
|
||||||
plan_development_q2 numeric,
|
|
||||||
plan_development_q3 numeric,
|
|
||||||
plan_development_q4 numeric,
|
|
||||||
plan_development_year numeric,
|
|
||||||
plan_total_year numeric,
|
|
||||||
approved_support_q1 numeric,
|
|
||||||
approved_support_q2 numeric,
|
|
||||||
approved_support_q3 numeric,
|
|
||||||
approved_support_q4 numeric,
|
|
||||||
approved_support_year numeric,
|
|
||||||
approved_development_q1 numeric,
|
|
||||||
approved_development_q2 numeric,
|
|
||||||
approved_development_q3 numeric,
|
|
||||||
approved_development_q4 numeric,
|
|
||||||
approved_development_year numeric,
|
|
||||||
approved_total_year numeric,
|
|
||||||
fact_support_q1 numeric,
|
|
||||||
fact_support_q2 numeric,
|
|
||||||
fact_support_q3 numeric,
|
|
||||||
fact_support_q4 numeric,
|
|
||||||
fact_support_year numeric,
|
|
||||||
fact_development_q1 numeric,
|
|
||||||
fact_development_q2 numeric,
|
|
||||||
fact_development_q3 numeric,
|
|
||||||
fact_development_q4 numeric,
|
|
||||||
fact_development_year numeric,
|
|
||||||
fact_total_year numeric,
|
|
||||||
corrected_support_q2 numeric,
|
|
||||||
corrected_support_q3 numeric,
|
|
||||||
corrected_support_q4 numeric,
|
|
||||||
corrected_development_q2 numeric,
|
|
||||||
corrected_development_q3 numeric,
|
|
||||||
corrected_development_q4 numeric
|
|
||||||
)
|
|
||||||
LANGUAGE sql
|
LANGUAGE sql
|
||||||
STABLE
|
STABLE
|
||||||
SET search_path TO 'v3', 'pg_catalog'
|
SET search_path TO 'v3', 'pg_catalog'
|
||||||
@ -1350,18 +1310,22 @@ summed AS (
|
|||||||
SELECT
|
SELECT
|
||||||
s.section_code, s.name,
|
s.section_code, s.name,
|
||||||
s.plan_support_q1, s.plan_support_q2, s.plan_support_q3, s.plan_support_q4, s.plan_support_year,
|
s.plan_support_q1, s.plan_support_q2, s.plan_support_q3, s.plan_support_q4, s.plan_support_year,
|
||||||
s.approved_support_q1, s.approved_support_q2, s.approved_support_q3, s.approved_support_q4, s.approved_support_year,
|
s.plan_development_q1, s.plan_development_q2, s.plan_development_q3, s.plan_development_q4, s.plan_development_year,
|
||||||
s.plan_support_year + s.plan_development_year AS plan_total_year,
|
s.plan_support_year + s.plan_development_year AS plan_total_year,
|
||||||
s.fact_support_q1, s.fact_support_q2, s.fact_support_q3, s.fact_support_q4, s.fact_support_year,
|
s.approved_support_q1, s.approved_support_q2, s.approved_support_q3, s.approved_support_q4, s.approved_support_year,
|
||||||
s.corrected_support_q2, s.corrected_support_q3, s.corrected_support_q4, s.plan_development_q1, s.plan_development_q2,
|
s.approved_development_q1, s.approved_development_q2, s.approved_development_q3, s.approved_development_q4, s.approved_development_year,
|
||||||
s.approved_support_year + s.approved_development_year AS approved_total_year,
|
s.approved_support_year + s.approved_development_year AS approved_total_year,
|
||||||
s.plan_development_q3, s.plan_development_q4, s.plan_development_year, s.approved_development_q1, s.approved_development_q2,
|
|
||||||
s.approved_development_q3, s.approved_development_q4, s.approved_development_year, s.fact_development_q1, s.fact_development_q2,
|
s.fact_support_q1, s.fact_support_q2, s.fact_support_q3, s.fact_support_q4, s.fact_support_year,
|
||||||
|
s.fact_development_q1, s.fact_development_q2, s.fact_development_q3, s.fact_development_q4, s.fact_development_year,
|
||||||
s.fact_support_year + s.fact_development_year AS fact_total_year,
|
s.fact_support_year + s.fact_development_year AS fact_total_year,
|
||||||
s.fact_development_q3, s.fact_development_q4, s.fact_development_year,
|
|
||||||
|
s.corrected_support_q2, s.corrected_support_q3, s.corrected_support_q4,
|
||||||
s.corrected_development_q2, s.corrected_development_q3, s.corrected_development_q4
|
s.corrected_development_q2, s.corrected_development_q3, s.corrected_development_q4
|
||||||
FROM summed AS s;
|
FROM summed AS s;
|
||||||
$function$;
|
$function$
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
-- =============================================================================
|
-- =============================================================================
|
||||||
-- main_format — VIEW
|
-- main_format — VIEW
|
||||||
|
|||||||
460
api/alembic/versions/sql/0022_summary2_contract_rows.sql
Normal file
460
api/alembic/versions/sql/0022_summary2_contract_rows.sql
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
-- «Свод 2»: детальные строки без агрегации по статьям.
|
||||||
|
-- ГО: FORM_1 + FORM_4; РФ: FORM_2 + FORM_3.
|
||||||
|
-- Пагинация выполняется по узкому набору ключей до тяжёлых JOIN и сборки JSON.
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_v3_budget_form_svod2
|
||||||
|
ON v3.budget_form (year, form_type_code, org_unit_id, id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_v3_budget_line_svod2
|
||||||
|
ON v3.budget_line (budget_form_id, id) INCLUDE (expense_item_id);
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS v3.v_svod2_go(integer, integer[]);
|
||||||
|
DROP FUNCTION IF EXISTS v3.v_svod2_go(integer, integer[], integer, integer);
|
||||||
|
DROP FUNCTION IF EXISTS v3.v_svod2_go(integer, integer[], integer, integer, varchar, varchar);
|
||||||
|
|
||||||
|
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'
|
||||||
|
)
|
||||||
|
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 quarter
|
||||||
|
), eligible_lines AS (
|
||||||
|
SELECT
|
||||||
|
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 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 IN ('FORM_1', 'FORM_4')
|
||||||
|
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.form_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,
|
||||||
|
pg.form_id,
|
||||||
|
pg.org_unit_id,
|
||||||
|
pg.org_name,
|
||||||
|
pg.smeta_type,
|
||||||
|
pg.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),
|
||||||
|
'q2', COALESCE(to_jsonb(q2) - 'line_id' - 'quarter', e.quarter),
|
||||||
|
'q3', COALESCE(to_jsonb(q3) - 'line_id' - 'quarter', e.quarter),
|
||||||
|
'q4', COALESCE(to_jsonb(q4) - 'line_id' - 'quarter', e.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$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION v3.v_svod2_go(integer, integer[], integer, integer, varchar, varchar) IS
|
||||||
|
'Свод 2 / ГО: договорные строки FORM_1 и FORM_4; пагинация до сборки JSON';
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS v3.v_svod2_rf(integer, integer[]);
|
||||||
|
DROP FUNCTION IF EXISTS v3.v_svod2_rf(integer, integer[], integer, integer);
|
||||||
|
DROP FUNCTION IF EXISTS v3.v_svod2_rf(integer, integer[], integer, integer, varchar, varchar);
|
||||||
|
|
||||||
|
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,
|
||||||
|
to_jsonb(jsonb_populate_record(NULL::v3.rf_project_report_quarter, '{}'::jsonb))
|
||||||
|
- 'rf_project_report_line_id' - 'quarter' AS report_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))
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'FORM_3'::varchar,
|
||||||
|
rrl.id,
|
||||||
|
rr.id,
|
||||||
|
p.org_unit_id,
|
||||||
|
ou.title,
|
||||||
|
ei.section_code,
|
||||||
|
ei.item_id,
|
||||||
|
CASE ei.sheet WHEN 'AHR' THEN 'АХР' WHEN 'CAP' THEN 'КВП'
|
||||||
|
WHEN 'OPER' THEN 'Операц' END::varchar
|
||||||
|
FROM v3.rf_project_report rr
|
||||||
|
JOIN v3.rf_project_year rpy ON rpy.id = rr.rf_project_year_id
|
||||||
|
JOIN v3.project p ON p.id = rr.project_id
|
||||||
|
JOIN v3.rf_project_report_line rrl ON rrl.rf_project_report_id = rr.id
|
||||||
|
JOIN v3.expense_item ei ON ei.id = rrl.expense_item_id
|
||||||
|
LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id
|
||||||
|
WHERE rpy.year = p_year AND ei.sheet IN ('AHR', 'CAP', 'OPER')
|
||||||
|
AND (p_org_unit_ids IS NULL OR cardinality(p_org_unit_ids) = 0
|
||||||
|
OR p.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
|
||||||
|
WHERE pg.form_type_code = 'FORM_2'
|
||||||
|
GROUP BY b.line_id
|
||||||
|
), form2_data AS (
|
||||||
|
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
|
||||||
|
WHERE pg.form_type_code = 'FORM_2'
|
||||||
|
), form3_data AS (
|
||||||
|
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', rrl.id,
|
||||||
|
'header', jsonb_build_object(
|
||||||
|
'section_code', ei.section_code, 'item_id', ei.item_id,
|
||||||
|
'num_group', ei.num_group_id, 'name', ei.name,
|
||||||
|
'expense_item_id', ei.id, 'project_id', rr.project_id,
|
||||||
|
'report_type', rr.report_type,
|
||||||
|
'smeta_direction', NULL,
|
||||||
|
'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', e.plan,
|
||||||
|
'sequestration', jsonb_build_object(
|
||||||
|
'DFIP', e.sequestration, 'SSP_GO', e.sequestration),
|
||||||
|
'reserve', e.reserve, 'contract_summary', e.contract_summary,
|
||||||
|
'approved', e.approved,
|
||||||
|
'allocation', e.allocation, 'collegial', e.collegial,
|
||||||
|
'ckk', e.ckk, 'contract_detail', e.contract_detail,
|
||||||
|
'booking', jsonb_build_array(e.booking),
|
||||||
|
'q1', COALESCE(to_jsonb(q1) - 'rf_project_report_line_id' - 'quarter', e.report_quarter),
|
||||||
|
'q2', COALESCE(to_jsonb(q2) - 'rf_project_report_line_id' - 'quarter', e.report_quarter),
|
||||||
|
'q3', COALESCE(to_jsonb(q3) - 'rf_project_report_line_id' - 'quarter', e.report_quarter),
|
||||||
|
'q4', COALESCE(to_jsonb(q4) - 'rf_project_report_line_id' - 'quarter', e.report_quarter)
|
||||||
|
) AS data
|
||||||
|
FROM page pg
|
||||||
|
JOIN v3.rf_project_report_line rrl ON rrl.id = pg.line_id
|
||||||
|
JOIN v3.rf_project_report rr ON rr.id = pg.source_id
|
||||||
|
AND rr.id = rrl.rf_project_report_id
|
||||||
|
JOIN v3.expense_item ei ON ei.id = rrl.expense_item_id
|
||||||
|
CROSS JOIN empty_sections e
|
||||||
|
LEFT JOIN v3.rf_project_report_quarter q1
|
||||||
|
ON q1.rf_project_report_line_id = rrl.id AND q1.quarter = 1
|
||||||
|
LEFT JOIN v3.rf_project_report_quarter q2
|
||||||
|
ON q2.rf_project_report_line_id = rrl.id AND q2.quarter = 2
|
||||||
|
LEFT JOIN v3.rf_project_report_quarter q3
|
||||||
|
ON q3.rf_project_report_line_id = rrl.id AND q3.quarter = 3
|
||||||
|
LEFT JOIN v3.rf_project_report_quarter q4
|
||||||
|
ON q4.rf_project_report_line_id = rrl.id AND q4.quarter = 4
|
||||||
|
WHERE pg.form_type_code = 'FORM_3'
|
||||||
|
), result_rows AS (
|
||||||
|
SELECT * FROM form2_data
|
||||||
|
UNION ALL
|
||||||
|
SELECT * FROM form3_data
|
||||||
|
)
|
||||||
|
SELECT rr.source_form, rr.source_id, rr.source_org_unit_id, rr.org_name, rr.smeta_type,
|
||||||
|
rr.source_line_id, rr.sort_order, rr.data
|
||||||
|
FROM result_rows rr
|
||||||
|
ORDER BY rr.sort_order;
|
||||||
|
$function$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION v3.v_svod2_rf(integer, integer[], integer, integer, varchar, varchar) IS
|
||||||
|
'Свод 2 / РФ: строки FORM_2 и FORM_3; пагинация до сборки JSON';
|
||||||
@ -1,11 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import Optional
|
from typing import Literal, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Response
|
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.api.v1.deps import get_current_active_user_with_set_db
|
from src.api.v1.deps import require_admin
|
||||||
from src.db.models.app_user import AppUser
|
from src.db.models.app_user import AppUser
|
||||||
from src.db.session import get_db
|
from src.db.session import get_db
|
||||||
from src.domain.schemas import BaseListResponse, SheetResponse
|
from src.domain.schemas import BaseListResponse, SheetResponse
|
||||||
@ -14,7 +14,7 @@ from src.services.summary_service import SummaryService
|
|||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
router = APIRouter(prefix="/svod/1", tags=["svod"])
|
router = APIRouter(prefix="/svod", tags=["svod"])
|
||||||
|
|
||||||
VALID_SUMMARY_SHEETS = {
|
VALID_SUMMARY_SHEETS = {
|
||||||
"FORM_1",
|
"FORM_1",
|
||||||
@ -23,6 +23,7 @@ VALID_SUMMARY_SHEETS = {
|
|||||||
"FORM_4",
|
"FORM_4",
|
||||||
"MAIN",
|
"MAIN",
|
||||||
}
|
}
|
||||||
|
VALID_SUMMARY2_SHEETS = {"GO", "RF"}
|
||||||
|
|
||||||
|
|
||||||
def _rows_to_sheet_response(result: list[tuple]) -> list[SheetResponse]:
|
def _rows_to_sheet_response(result: list[tuple]) -> list[SheetResponse]:
|
||||||
@ -46,14 +47,14 @@ def _parse_org_ids(org_ids: Optional[str]) -> Optional[list[int]]:
|
|||||||
return parsed or None
|
return parsed or None
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{sheet}/{year}")
|
@router.get("/1/{sheet}/{year}")
|
||||||
async def get_summary(
|
async def get_summary(
|
||||||
sheet: str,
|
sheet: str,
|
||||||
year: int,
|
year: int,
|
||||||
response: Response,
|
response: Response,
|
||||||
org_ids: Optional[str] = None,
|
org_ids: Optional[str] = None,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
current_user: AppUser = Depends(require_admin),
|
||||||
) -> BaseListResponse[SheetResponse]:
|
) -> BaseListResponse[SheetResponse]:
|
||||||
if sheet not in VALID_SUMMARY_SHEETS:
|
if sheet not in VALID_SUMMARY_SHEETS:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -86,3 +87,58 @@ async def get_summary(
|
|||||||
count=len(rows),
|
count=len(rows),
|
||||||
result=_rows_to_sheet_response(rows),
|
result=_rows_to_sheet_response(rows),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/2/{sheet}/{year}")
|
||||||
|
async def get_summary2(
|
||||||
|
sheet: str,
|
||||||
|
year: int,
|
||||||
|
response: Response,
|
||||||
|
org_ids: Optional[str] = None,
|
||||||
|
offset: Optional[int] = Query(default=None, ge=0),
|
||||||
|
limit: Optional[int] = Query(default=None, ge=1),
|
||||||
|
sort_by: Literal["org_name", "section_code"] = "org_name",
|
||||||
|
sort_direction: Literal["asc", "desc"] = "asc",
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(require_admin),
|
||||||
|
) -> BaseListResponse[SheetResponse]:
|
||||||
|
sheet = sheet.upper()
|
||||||
|
if sheet not in VALID_SUMMARY2_SHEETS:
|
||||||
|
raise HTTPException(
|
||||||
|
404,
|
||||||
|
f"Лист свода {sheet} должен быть одним из: {', '.join(sorted(VALID_SUMMARY2_SHEETS))}",
|
||||||
|
)
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
summary_service = SummaryService(db)
|
||||||
|
rows = await summary_service.get_svod2_rows(
|
||||||
|
year=year,
|
||||||
|
sheet=sheet,
|
||||||
|
org_ids=_parse_org_ids(org_ids),
|
||||||
|
offset=offset,
|
||||||
|
limit=limit,
|
||||||
|
sort_by=sort_by,
|
||||||
|
sort_direction=sort_direction,
|
||||||
|
)
|
||||||
|
db_ms = (time.perf_counter() - t0) * 1000
|
||||||
|
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
||||||
|
|
||||||
|
extra = {
|
||||||
|
"type": "db_time",
|
||||||
|
"time_ms": f"{db_ms:.2f}",
|
||||||
|
"handler": "get_summary2",
|
||||||
|
"sheet": sheet,
|
||||||
|
"year": year,
|
||||||
|
"org_ids": org_ids,
|
||||||
|
"offset": offset,
|
||||||
|
"limit": limit,
|
||||||
|
"sort_by": sort_by,
|
||||||
|
"sort_direction": sort_direction,
|
||||||
|
"user": current_user.id,
|
||||||
|
}
|
||||||
|
logger.info(f"db_time: {extra}", extra=extra)
|
||||||
|
|
||||||
|
return BaseListResponse(
|
||||||
|
count=len(rows),
|
||||||
|
result=_rows_to_sheet_response(rows),
|
||||||
|
)
|
||||||
|
|||||||
@ -28,3 +28,49 @@ class SummaryRepository:
|
|||||||
{"year": year, "sheet": sheet, "org_ids": org_ids},
|
{"year": year, "sheet": sheet, "org_ids": org_ids},
|
||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
|
async def get_svod2_rows(
|
||||||
|
self,
|
||||||
|
year: int,
|
||||||
|
sheet: str,
|
||||||
|
org_ids: list[int] | None = None,
|
||||||
|
offset: int | None = None,
|
||||||
|
limit: int | None = None,
|
||||||
|
sort_by: str = "org_name",
|
||||||
|
sort_direction: str = "asc",
|
||||||
|
) -> list[tuple]:
|
||||||
|
function_name = {
|
||||||
|
"GO": "v3.v_svod2_go",
|
||||||
|
"RF": "v3.v_svod2_rf",
|
||||||
|
}[sheet]
|
||||||
|
query = text(
|
||||||
|
f"""
|
||||||
|
SELECT
|
||||||
|
'INPUT'::VARCHAR AS row_type,
|
||||||
|
3 AS depth,
|
||||||
|
sort_order,
|
||||||
|
data
|
||||||
|
FROM {function_name}(
|
||||||
|
CAST(:year AS INT),
|
||||||
|
CAST(:org_ids AS INT[]),
|
||||||
|
CAST(:offset AS INT),
|
||||||
|
CAST(:limit AS INT),
|
||||||
|
CAST(:sort_by AS VARCHAR),
|
||||||
|
CAST(:sort_direction AS VARCHAR)
|
||||||
|
)
|
||||||
|
ORDER BY sort_order
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
await self.db.execute(
|
||||||
|
query,
|
||||||
|
{
|
||||||
|
"year": year,
|
||||||
|
"org_ids": org_ids,
|
||||||
|
"offset": offset,
|
||||||
|
"limit": limit,
|
||||||
|
"sort_by": sort_by,
|
||||||
|
"sort_direction": sort_direction,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
|||||||
@ -19,3 +19,23 @@ class SummaryService:
|
|||||||
sheet=sheet,
|
sheet=sheet,
|
||||||
org_ids=org_ids,
|
org_ids=org_ids,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def get_svod2_rows(
|
||||||
|
self,
|
||||||
|
year: int,
|
||||||
|
sheet: str,
|
||||||
|
org_ids: list[int] | None = None,
|
||||||
|
offset: int | None = None,
|
||||||
|
limit: int | None = None,
|
||||||
|
sort_by: str = "org_name",
|
||||||
|
sort_direction: str = "asc",
|
||||||
|
) -> list[tuple]:
|
||||||
|
return await self.summary_repo.get_svod2_rows(
|
||||||
|
year=year,
|
||||||
|
sheet=sheet,
|
||||||
|
org_ids=org_ids,
|
||||||
|
offset=offset,
|
||||||
|
limit=limit,
|
||||||
|
sort_by=sort_by,
|
||||||
|
sort_direction=sort_direction,
|
||||||
|
)
|
||||||
|
|||||||
@ -38,3 +38,104 @@ def test_summary_sheet_unknown(client, admin_tokens, auth_headers):
|
|||||||
headers=auth_headers(admin_tokens),
|
headers=auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("sheet", ["GO", "RF"])
|
||||||
|
def test_summary2_sheets_smoke(client, admin_tokens, auth_headers, sheet):
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1/svod/2/{sheet}/2026",
|
||||||
|
headers=auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, f"sheet={sheet}: {response.text}"
|
||||||
|
payload = response.json()
|
||||||
|
assert "result" in payload
|
||||||
|
assert "count" in payload
|
||||||
|
assert isinstance(payload["result"], list)
|
||||||
|
if payload["result"]:
|
||||||
|
row = payload["result"][0]
|
||||||
|
assert row["row_type"] == "INPUT"
|
||||||
|
assert row["depth"] == 3
|
||||||
|
assert "sort_order" in row
|
||||||
|
assert "data" in row
|
||||||
|
data = row["data"]
|
||||||
|
header = data["header"]
|
||||||
|
assert {
|
||||||
|
"org_name",
|
||||||
|
"source_id",
|
||||||
|
"smeta_type",
|
||||||
|
"source_form",
|
||||||
|
"source_org_unit_id",
|
||||||
|
"smeta_direction",
|
||||||
|
} <= header.keys()
|
||||||
|
assert "source_sheet" not in data
|
||||||
|
assert "source_sheet" not in header
|
||||||
|
assert {
|
||||||
|
"plan",
|
||||||
|
"sequestration",
|
||||||
|
"reserve",
|
||||||
|
"contract_summary",
|
||||||
|
"allocation",
|
||||||
|
"collegial",
|
||||||
|
"ckk",
|
||||||
|
"contract_detail",
|
||||||
|
"booking",
|
||||||
|
"q1",
|
||||||
|
"q2",
|
||||||
|
"q3",
|
||||||
|
"q4",
|
||||||
|
} <= data.keys()
|
||||||
|
|
||||||
|
|
||||||
|
def test_summary2_sheet_with_org_ids(client, admin_tokens, auth_headers):
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/svod/2/GO/2026?org_ids=1,2,3",
|
||||||
|
headers=auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_summary2_pagination(client, admin_tokens, auth_headers):
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/svod/2/GO/2026?offset=0&limit=2",
|
||||||
|
headers=auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert payload["count"] <= 2
|
||||||
|
assert len(payload["result"]) <= 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("sort_by", ["org_name", "section_code"])
|
||||||
|
@pytest.mark.parametrize("sort_direction", ["asc", "desc"])
|
||||||
|
def test_summary2_sorting(
|
||||||
|
client,
|
||||||
|
admin_tokens,
|
||||||
|
auth_headers,
|
||||||
|
sort_by,
|
||||||
|
sort_direction,
|
||||||
|
):
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1/svod/2/GO/2026?limit=2&sort_by={sort_by}&sort_direction={sort_direction}",
|
||||||
|
headers=auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"query",
|
||||||
|
["offset=-1", "limit=0", "sort_by=bad", "sort_direction=bad"],
|
||||||
|
)
|
||||||
|
def test_summary2_invalid_pagination(client, admin_tokens, auth_headers, query):
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1/svod/2/GO/2026?{query}",
|
||||||
|
headers=auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
def test_summary2_sheet_unknown(client, admin_tokens, auth_headers):
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/svod/2/unknown/2026",
|
||||||
|
headers=auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user