Compare commits
9 Commits
d5728c8e08
...
056d00b2b4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
056d00b2b4 | ||
| e2481d8094 | |||
| 18faf6a724 | |||
| cd2882dd61 | |||
| 1e02b0ca61 | |||
| 9500071fa4 | |||
| d6da54ecf4 | |||
| 94c18b4ca7 | |||
| 909c16bf9b |
15
api/alembic/versions/0037_form2_ahr_limit_depth_fix.py
Normal file
15
api/alembic/versions/0037_form2_ahr_limit_depth_fix.py
Normal file
@ -0,0 +1,15 @@
|
||||
from sql_migration import run_sql_file_migration
|
||||
|
||||
|
||||
revision = "0037"
|
||||
down_revision = "0036"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
run_sql_file_migration("0037_form2_ahr_limit_depth_fix.sql")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
15
api/alembic/versions/0038_form3_expense_item_links.py
Normal file
15
api/alembic/versions/0038_form3_expense_item_links.py
Normal file
@ -0,0 +1,15 @@
|
||||
from sql_migration import run_sql_file_migration
|
||||
|
||||
|
||||
revision = "0038"
|
||||
down_revision = "0037"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
run_sql_file_migration("0038_form3_expense_item_links.sql")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
15
api/alembic/versions/0039_budget_form_delete_workflow.py
Normal file
15
api/alembic/versions/0039_budget_form_delete_workflow.py
Normal file
@ -0,0 +1,15 @@
|
||||
from sql_migration import run_sql_file_migration
|
||||
|
||||
|
||||
revision = "0039"
|
||||
down_revision = "0038"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
run_sql_file_migration("0039_budget_form_delete_workflow.sql")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
15
api/alembic/versions/0040_ahr_limit_delete_guard.py
Normal file
15
api/alembic/versions/0040_ahr_limit_delete_guard.py
Normal file
@ -0,0 +1,15 @@
|
||||
from sql_migration import run_sql_file_migration
|
||||
|
||||
|
||||
revision = "0040"
|
||||
down_revision = "0039"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
run_sql_file_migration("0040_ahr_limit_delete_guard.sql")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
15
api/alembic/versions/0041_svod2_rf_form2_only.py
Normal file
15
api/alembic/versions/0041_svod2_rf_form2_only.py
Normal 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
|
||||
113
api/alembic/versions/sql/0037_form2_ahr_limit_depth_fix.sql
Normal file
113
api/alembic/versions/sql/0037_form2_ahr_limit_depth_fix.sql
Normal file
@ -0,0 +1,113 @@
|
||||
-- After 0033 some AHR expense items with num_group_id became depth=4.
|
||||
-- AHR_LIMIT synchronization still required depth=3, so limit rows such as
|
||||
-- "принтеры", "МФУ", and "копировальные аппараты" stopped matching
|
||||
-- expense_item.num_group_id = '204620030'.
|
||||
--
|
||||
-- The same three limit rows also use a special quarterly amount formula:
|
||||
-- qty * 0.57 * 10900 / 4. Other limit rows keep the previous formula.
|
||||
|
||||
CREATE OR REPLACE FUNCTION v3._form2_ahr_limit_amount(
|
||||
p_name character varying,
|
||||
p_unit character varying,
|
||||
p_limit_with_vat numeric,
|
||||
p_qty numeric
|
||||
)
|
||||
RETURNS numeric
|
||||
LANGUAGE sql
|
||||
IMMUTABLE
|
||||
AS $function$
|
||||
SELECT COALESCE(p_qty, 0) *
|
||||
CASE
|
||||
WHEN regexp_replace(
|
||||
COALESCE(p_name, ''),
|
||||
'^[[:space:]]*-[[:space:]]*',
|
||||
''
|
||||
) IN ('принтеры', 'МФУ', 'копировальные аппараты')
|
||||
THEN 0.57 * 10900 / 4
|
||||
ELSE COALESCE(p_limit_with_vat, 0) *
|
||||
CASE
|
||||
WHEN p_unit LIKE '%/год' THEN 0.25
|
||||
WHEN p_unit LIKE '%/месяц' THEN 3
|
||||
ELSE 1
|
||||
END
|
||||
END;
|
||||
$function$;
|
||||
|
||||
DO $do$
|
||||
DECLARE
|
||||
v_signature regprocedure;
|
||||
v_sql text;
|
||||
v_next_sql text;
|
||||
BEGIN
|
||||
FOREACH v_signature IN ARRAY ARRAY[
|
||||
'v3.v_form2_view(integer,character varying,text[])'::regprocedure,
|
||||
'v3.sync_form2_ahr_satellite_lines(integer)'::regprocedure,
|
||||
'v3.sync_form2_ahr_satellite_line(integer,character varying,integer)'::regprocedure
|
||||
]
|
||||
LOOP
|
||||
v_sql := pg_get_functiondef(v_signature);
|
||||
|
||||
v_next_sql := regexp_replace(
|
||||
v_sql,
|
||||
$re$COALESCE\(fl\.qty_q([1-4]),[[:space:]]*0\)[[:space:]]*\*[[:space:]]*COALESCE\(lt\.limit_with_vat,[[:space:]]*0\)[[:space:]]*\*[[:space:]]*\(?[[:space:]]*CASE[[:space:]]+WHEN[[:space:]]+lt\.unit[[:space:]]+LIKE[[:space:]]+'%/год'[[:space:]]+THEN[[:space:]]+0\.25[[:space:]]+WHEN[[:space:]]+lt\.unit[[:space:]]+LIKE[[:space:]]+'%/месяц'[[:space:]]+THEN[[:space:]]+3[[:space:]]+ELSE[[:space:]]+1[[:space:]]+END[[:space:]]*\)?$re$,
|
||||
$replace$v3._form2_ahr_limit_amount(lt.name, lt.unit, lt.limit_with_vat, fl.qty_q\1)$replace$,
|
||||
'g'
|
||||
);
|
||||
|
||||
IF v_next_sql = v_sql THEN
|
||||
RAISE EXCEPTION 'Expected AHR limit amount expression was not found in %', v_signature;
|
||||
END IF;
|
||||
|
||||
EXECUTE v_next_sql;
|
||||
END LOOP;
|
||||
END;
|
||||
$do$;
|
||||
|
||||
DO $do$
|
||||
DECLARE
|
||||
v_signature regprocedure;
|
||||
v_sql text;
|
||||
v_next_sql text;
|
||||
BEGIN
|
||||
FOREACH v_signature IN ARRAY ARRAY[
|
||||
'v3.sync_form2_ahr_satellite_lines(integer)'::regprocedure,
|
||||
'v3.sync_form2_ahr_satellite_line(integer,character varying,integer)'::regprocedure,
|
||||
'v3.get_form2_ahr_satellite_delta(integer,character varying,integer)'::regprocedure,
|
||||
'v3.is_form2_ahr_satellite_line(integer,integer)'::regprocedure
|
||||
]
|
||||
LOOP
|
||||
v_sql := pg_get_functiondef(v_signature);
|
||||
|
||||
v_next_sql := regexp_replace(
|
||||
v_sql,
|
||||
'AND\s+ei\.depth\s*=\s*3\s+AND\s*\(\s*ei\.item_id\s*=\s*lt\.expense_item_code\s+OR\s+ei\.num_group_id\s*=\s*lt\.expense_item_code\s*\)',
|
||||
E'AND (\n (ei.depth = 3 AND ei.item_id = lt.expense_item_code)\n OR ei.num_group_id = lt.expense_item_code\n )',
|
||||
'g'
|
||||
);
|
||||
|
||||
IF v_next_sql = v_sql THEN
|
||||
IF v_sql ~ 'ei\.depth[[:space:]]*=[[:space:]]*3[[:space:]]+AND[[:space:]]+ei\.item_id[[:space:]]*=[[:space:]]*lt\.expense_item_code[[:space:]]*\)[[:space:]]+OR[[:space:]]+ei\.num_group_id[[:space:]]*=[[:space:]]*lt\.expense_item_code' THEN
|
||||
CONTINUE;
|
||||
END IF;
|
||||
|
||||
RAISE EXCEPTION 'Expected depth=3 limit-template matcher was not found in %', v_signature;
|
||||
END IF;
|
||||
|
||||
EXECUTE v_next_sql;
|
||||
END LOOP;
|
||||
END;
|
||||
$do$;
|
||||
|
||||
DO $do$
|
||||
DECLARE
|
||||
v_form_id integer;
|
||||
BEGIN
|
||||
FOR v_form_id IN
|
||||
SELECT id
|
||||
FROM v3.budget_form
|
||||
WHERE form_type_code = 'FORM_2'
|
||||
LOOP
|
||||
PERFORM v3.sync_form2_ahr_satellite_lines(v_form_id);
|
||||
END LOOP;
|
||||
END;
|
||||
$do$;
|
||||
22
api/alembic/versions/sql/0038_form3_expense_item_links.sql
Normal file
22
api/alembic/versions/sql/0038_form3_expense_item_links.sql
Normal file
@ -0,0 +1,22 @@
|
||||
insert into v3.expense_item_form_type (expense_item_id, form_type_code)
|
||||
values
|
||||
(84, 'FORM_3'),
|
||||
(85, 'FORM_3'),
|
||||
(86, 'FORM_3'),
|
||||
(87, 'FORM_3'),
|
||||
(88, 'FORM_3'),
|
||||
(89, 'FORM_3'),
|
||||
(90, 'FORM_3'),
|
||||
(91, 'FORM_3'),
|
||||
(92, 'FORM_3'),
|
||||
(93, 'FORM_3'),
|
||||
(94, 'FORM_3'),
|
||||
(95, 'FORM_3'),
|
||||
(97, 'FORM_3'),
|
||||
(98, 'FORM_3'),
|
||||
(99, 'FORM_3'),
|
||||
(100, 'FORM_3'),
|
||||
(101, 'FORM_3'),
|
||||
(83, 'FORM_3'),
|
||||
(96, 'FORM_3')
|
||||
on conflict (expense_item_id, form_type_code) do nothing;
|
||||
@ -0,0 +1,75 @@
|
||||
ALTER TABLE v3.budget_form
|
||||
ADD COLUMN IF NOT EXISTS to_delete boolean NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS deleted_by integer NULL REFERENCES v3.app_user(id),
|
||||
ADD COLUMN IF NOT EXISTS deleted_at timestamp with time zone NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_v3_budget_form_to_delete
|
||||
ON v3.budget_form (to_delete);
|
||||
|
||||
CREATE OR REPLACE FUNCTION v3.hard_delete_budget_form(p_form_id integer)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM v3.budget_form WHERE id = p_form_id) THEN
|
||||
RAISE EXCEPTION 'budget_form id=% не существует', p_form_id;
|
||||
END IF;
|
||||
|
||||
DELETE FROM v3.booking
|
||||
WHERE line_id IN (SELECT id FROM v3.budget_line WHERE budget_form_id = p_form_id);
|
||||
|
||||
DELETE FROM v3.sequestration
|
||||
WHERE line_id IN (SELECT id FROM v3.budget_line WHERE budget_form_id = p_form_id);
|
||||
|
||||
DELETE FROM v3.budget_line_quarter
|
||||
WHERE line_id IN (SELECT id FROM v3.budget_line WHERE budget_form_id = p_form_id);
|
||||
|
||||
DELETE FROM v3.rent_detail
|
||||
WHERE line_id IN (SELECT id FROM v3.budget_line WHERE budget_form_id = p_form_id);
|
||||
|
||||
DELETE FROM v3.utility_detail
|
||||
WHERE line_id IN (SELECT id FROM v3.budget_line WHERE budget_form_id = p_form_id);
|
||||
|
||||
DELETE FROM v3.security_detail
|
||||
WHERE line_id IN (SELECT id FROM v3.budget_line WHERE budget_form_id = p_form_id);
|
||||
|
||||
DELETE FROM v3.contract_detail
|
||||
WHERE line_id IN (SELECT id FROM v3.budget_line WHERE budget_form_id = p_form_id);
|
||||
|
||||
DELETE FROM v3.contract_summary
|
||||
WHERE line_id IN (SELECT id FROM v3.budget_line WHERE budget_form_id = p_form_id);
|
||||
|
||||
DELETE FROM v3.plan
|
||||
WHERE line_id IN (SELECT id FROM v3.budget_line WHERE budget_form_id = p_form_id);
|
||||
|
||||
DELETE FROM v3.reserve
|
||||
WHERE line_id IN (SELECT id FROM v3.budget_line WHERE budget_form_id = p_form_id);
|
||||
|
||||
DELETE FROM v3.allocation
|
||||
WHERE line_id IN (SELECT id FROM v3.budget_line WHERE budget_form_id = p_form_id);
|
||||
|
||||
DELETE FROM v3.ckk
|
||||
WHERE line_id IN (SELECT id FROM v3.budget_line WHERE budget_form_id = p_form_id);
|
||||
|
||||
DELETE FROM v3.collegial_approval
|
||||
WHERE line_id IN (SELECT id FROM v3.budget_line WHERE budget_form_id = p_form_id);
|
||||
|
||||
DELETE FROM v3.fixed_asset_report
|
||||
WHERE budget_form_id = p_form_id;
|
||||
|
||||
DELETE FROM v3.form_limit
|
||||
WHERE budget_form_id = p_form_id;
|
||||
|
||||
DELETE FROM v3.form_phase
|
||||
WHERE budget_form_id = p_form_id;
|
||||
|
||||
DELETE FROM v3.budget_line
|
||||
WHERE budget_form_id = p_form_id;
|
||||
|
||||
DELETE FROM v3.form4_project
|
||||
WHERE form_id = p_form_id;
|
||||
|
||||
DELETE FROM v3.budget_form
|
||||
WHERE id = p_form_id;
|
||||
END;
|
||||
$function$;
|
||||
145
api/alembic/versions/sql/0040_ahr_limit_delete_guard.sql
Normal file
145
api/alembic/versions/sql/0040_ahr_limit_delete_guard.sql
Normal file
@ -0,0 +1,145 @@
|
||||
CREATE OR REPLACE FUNCTION v3.del_budget_line(p_id integer, p_direction character varying DEFAULT NULL::character varying, p_sheet character varying DEFAULT NULL::character varying)
|
||||
RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_form_id INT;
|
||||
v_org_id INT;
|
||||
v_sheet VARCHAR;
|
||||
v_sat_table TEXT;
|
||||
v_eid INT;
|
||||
v_line_id INT;
|
||||
v_ei_path INT[];
|
||||
BEGIN
|
||||
IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN
|
||||
v_sat_table := CASE p_sheet
|
||||
WHEN 'AHR_RENT' THEN 'rent_detail'
|
||||
WHEN 'AHR_UTILITY' THEN 'utility_detail'
|
||||
WHEN 'AHR_SECURITY' THEN 'security_detail'
|
||||
END;
|
||||
|
||||
EXECUTE format(
|
||||
'SELECT bl.budget_form_id FROM v3.%I s
|
||||
JOIN v3.budget_line bl ON bl.id = s.line_id
|
||||
WHERE s.id = $1', v_sat_table
|
||||
) USING p_id INTO v_form_id;
|
||||
|
||||
IF v_form_id IS NULL THEN
|
||||
RAISE EXCEPTION '%.id=% не существует', v_sat_table, p_id;
|
||||
END IF;
|
||||
|
||||
SELECT m.target_line_id
|
||||
INTO v_line_id
|
||||
FROM v3.form2_ahr_satellite_line_source m
|
||||
WHERE m.source_sheet = p_sheet
|
||||
AND m.source_id = p_id;
|
||||
|
||||
EXECUTE format('DELETE FROM v3.%I WHERE id = $1', v_sat_table) USING p_id;
|
||||
|
||||
IF v_line_id IS NOT NULL THEN
|
||||
SELECT bl.expense_item_id
|
||||
INTO v_eid
|
||||
FROM v3.budget_line bl
|
||||
WHERE bl.id = v_line_id;
|
||||
|
||||
DELETE FROM v3.plan WHERE line_id = v_line_id;
|
||||
DELETE FROM v3.budget_line_quarter WHERE line_id = v_line_id;
|
||||
DELETE FROM v3.budget_line WHERE id = v_line_id;
|
||||
|
||||
SELECT org_unit_id INTO v_org_id FROM v3.budget_form WHERE id = v_form_id;
|
||||
|
||||
PERFORM v3.log_event(
|
||||
'ROW_DELETE', 'ROW',
|
||||
jsonb_build_object(
|
||||
'sheet', 'AHR',
|
||||
'line_id', v_line_id,
|
||||
'entity_id', v_line_id,
|
||||
'expense_item_id', v_eid,
|
||||
'direction', 'Support',
|
||||
'source_sheet', p_sheet,
|
||||
'source_id', p_id
|
||||
),
|
||||
v_form_id, NULL, v_org_id
|
||||
);
|
||||
END IF;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
SELECT bl.budget_form_id, ei.sheet, bl.expense_item_id
|
||||
INTO v_form_id, v_sheet, v_eid
|
||||
FROM v3.budget_line bl
|
||||
JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
|
||||
WHERE bl.id = p_id;
|
||||
|
||||
IF v_form_id IS NULL THEN
|
||||
RAISE EXCEPTION 'budget_line id=% не существует', p_id;
|
||||
END IF;
|
||||
|
||||
IF v_sheet = 'AHR' AND (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM v3.form2_ahr_satellite_line_source m
|
||||
WHERE m.target_line_id = p_id
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM v3.budget_line bl
|
||||
JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
|
||||
JOIN v3.limit_template lt
|
||||
ON lt.row_type = 'LEAF'
|
||||
AND (
|
||||
(ei.depth = 3 AND ei.item_id = lt.expense_item_code)
|
||||
OR ei.num_group_id = lt.expense_item_code
|
||||
)
|
||||
WHERE bl.id = p_id
|
||||
AND bl.budget_form_id = v_form_id
|
||||
AND ei.sheet = 'AHR'
|
||||
AND ei.direction = 'Support'
|
||||
)
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Строку АХР, созданную из листа-сателлита или АХР Лимиты, нельзя удалить напрямую'
|
||||
USING ERRCODE = '23503',
|
||||
HINT = 'Удалите строку на исходном листе АХР Аренда / АХР Коммунальные услуги / АХР Безопасность либо измените строку на листе АХР Лимиты';
|
||||
END IF;
|
||||
|
||||
v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]);
|
||||
|
||||
DELETE FROM v3.plan WHERE line_id = p_id;
|
||||
DELETE FROM v3.sequestration WHERE line_id = p_id;
|
||||
DELETE FROM v3.reserve WHERE line_id = p_id;
|
||||
DELETE FROM v3.allocation WHERE line_id = p_id;
|
||||
DELETE FROM v3.contract_summary WHERE line_id = p_id;
|
||||
DELETE FROM v3.collegial_approval WHERE line_id = p_id;
|
||||
DELETE FROM v3.ckk WHERE line_id = p_id;
|
||||
DELETE FROM v3.contract_detail WHERE line_id = p_id;
|
||||
DELETE FROM v3.booking WHERE line_id = p_id;
|
||||
DELETE FROM v3.budget_line_quarter WHERE line_id = p_id;
|
||||
DELETE FROM v3.rent_detail WHERE line_id = p_id;
|
||||
DELETE FROM v3.utility_detail WHERE line_id = p_id;
|
||||
DELETE FROM v3.security_detail WHERE line_id = p_id;
|
||||
|
||||
DELETE FROM v3.budget_line WHERE id = p_id;
|
||||
|
||||
SELECT org_unit_id INTO v_org_id FROM v3.budget_form WHERE id = v_form_id;
|
||||
|
||||
PERFORM v3.log_event(
|
||||
'ROW_DELETE', 'ROW',
|
||||
jsonb_build_object(
|
||||
'sheet', v_sheet,
|
||||
'line_id', p_id,
|
||||
'entity_id', p_id,
|
||||
'expense_item_id', v_eid,
|
||||
'direction', p_direction
|
||||
),
|
||||
v_form_id, NULL, v_org_id
|
||||
);
|
||||
|
||||
RETURN QUERY
|
||||
SELECT v.row_type, v.depth, v.sort_order, v.data
|
||||
FROM v3.v_form_view(v_form_id, v_sheet, NULL, p_direction) v
|
||||
WHERE v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
|
||||
AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path)
|
||||
ORDER BY v.sort_order;
|
||||
END;
|
||||
$function$;
|
||||
|
||||
299
api/alembic/versions/sql/0041_svod2_rf_form2_only.sql
Normal file
299
api/alembic/versions/sql/0041_svod2_rf_form2_only.sql
Normal 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$;
|
||||
@ -8,8 +8,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.services.org_unit_service import OrgUnitService
|
||||
from src.db.models.form_type import FormTypeEnum
|
||||
from src.db.models.role import UserRoleEnum
|
||||
from src.services.sheet_service import SheetService
|
||||
from src.domain.schemas import AddLineSchema, BaseListResponse, BaseSingleResponse, BudgetFormResponse, CellPatch, CellsPatch, DirectionSchemaEnum, FormCreateSchema, SheetFormTypeResponse, SheetResponse
|
||||
from src.domain.schemas import AddLineSchema, BaseListResponse, BaseSingleResponse, ResponseBase, BudgetFormResponse, CellPatch, CellsPatch, DirectionSchemaEnum, FormCreateSchema, SheetFormTypeResponse, SheetResponse
|
||||
from src.db.models.app_user import AppUser
|
||||
from src.services.budget_form_service import BudgetFormService
|
||||
from src.api.v1.deps import get_current_active_user_with_set_db, require_admin
|
||||
@ -80,6 +81,7 @@ async def get_forms(
|
||||
org_units: str | None = None,
|
||||
form_type: Literal["FORM_1", "FORM_2", "FORM_4"] | None = None,
|
||||
year: int | None = None,
|
||||
delete_status: Literal["active", "marked", "all"] | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||
) -> BaseListResponse[BudgetFormResponse]:
|
||||
@ -95,6 +97,7 @@ async def get_forms(
|
||||
form_type=form_type,
|
||||
year=year,
|
||||
load_org=True,
|
||||
delete_status=delete_status,
|
||||
)
|
||||
return BaseListResponse(
|
||||
result = [
|
||||
@ -112,12 +115,15 @@ async def get_form(
|
||||
) -> BaseSingleResponse[BudgetFormResponse]:
|
||||
bf_service = BudgetFormService(db)
|
||||
form = await bf_service.get(
|
||||
user=current_user,
|
||||
user=current_user,
|
||||
budget_form_id=form_id,
|
||||
load_org=True,
|
||||
include_to_delete=current_user.role_id == UserRoleEnum.ADMIN,
|
||||
)
|
||||
if not form:
|
||||
raise HTTPException(404, f"Форма {form_id} не найдена")
|
||||
return BaseSingleResponse(
|
||||
result = BudgetFormResponse.model_validate(form),
|
||||
result=BudgetFormResponse.model_validate(form),
|
||||
)
|
||||
|
||||
|
||||
@ -464,6 +470,7 @@ async def add_form(
|
||||
form_type_code=form.form_type_code,
|
||||
year=form.year,
|
||||
org_unit_ids=org_unit_ids,
|
||||
user=current_user,
|
||||
)
|
||||
|
||||
db_ms = (time.perf_counter() - t0) * 1000
|
||||
@ -485,3 +492,36 @@ async def add_form(
|
||||
result=[BudgetFormResponse.model_validate(new_form) for new_form in new_forms],
|
||||
count=len(new_forms),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{form_id}/mark-delete")
|
||||
async def mark_form_for_deletion(
|
||||
form_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: AppUser = Depends(require_admin),
|
||||
) -> BaseSingleResponse[BudgetFormResponse]:
|
||||
bf_service = BudgetFormService(db)
|
||||
form = await bf_service.mark_for_deletion(budget_form_id=form_id, user=current_user)
|
||||
return BaseSingleResponse(result=BudgetFormResponse.model_validate(form))
|
||||
|
||||
|
||||
@router.post("/{form_id}/restore")
|
||||
async def restore_form(
|
||||
form_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: AppUser = Depends(require_admin),
|
||||
) -> BaseSingleResponse[BudgetFormResponse]:
|
||||
bf_service = BudgetFormService(db)
|
||||
form = await bf_service.restore(budget_form_id=form_id, user=current_user)
|
||||
return BaseSingleResponse(result=BudgetFormResponse.model_validate(form))
|
||||
|
||||
|
||||
@router.delete("/{form_id}/hard")
|
||||
async def hard_delete_form(
|
||||
form_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: AppUser = Depends(require_admin),
|
||||
) -> ResponseBase:
|
||||
bf_service = BudgetFormService(db)
|
||||
await bf_service.hard_delete(budget_form_id=form_id, user=current_user)
|
||||
return ResponseBase(success=True, message="Задача полностью удалена")
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from datetime import datetime
|
||||
import typing
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src.db.models.form_type import FormTypeEnum
|
||||
@ -34,6 +34,9 @@ class BudgetForm(Base):
|
||||
org_unit_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("v3.org_unit.id", name="fk_v3_budget_form_org_unit")
|
||||
)
|
||||
to_delete: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default="false")
|
||||
deleted_by: Mapped[int | None] = mapped_column(Integer, ForeignKey("v3.app_user.id"))
|
||||
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
form_type: Mapped["FormType"] = relationship(
|
||||
"FormType", #back_populates="budget_forms"
|
||||
|
||||
@ -330,6 +330,9 @@ class BudgetFormResponse(BaseModel):
|
||||
form_type_code: FormTypeSchemaEnum
|
||||
year: int
|
||||
org_unit_id: int
|
||||
to_delete: bool = False
|
||||
deleted_by: int | None = None
|
||||
deleted_at: datetime | None = None
|
||||
org_unit: OrgUnitSchema | None = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from sqlalchemy import func, select, text
|
||||
from sqlalchemy import func, select, text, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.db.models.form_type import FormTypeEnum
|
||||
@ -6,20 +6,20 @@ from src.db.models.budget_form import BudgetForm
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
|
||||
|
||||
class BudgetFormRepository:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def get_list(
|
||||
self,
|
||||
offset: int | None = None,
|
||||
self,
|
||||
offset: int | None = None,
|
||||
limit: int | None = None,
|
||||
org_unit: int | list[int] | None = None,
|
||||
with_count: bool = False,
|
||||
load_org: bool = False,
|
||||
form_type: FormTypeEnum | str | None = None,
|
||||
year: int | None = None,
|
||||
delete_status: str = "active",
|
||||
) -> list[BudgetForm] | tuple[int, list[BudgetForm]]:
|
||||
if with_count:
|
||||
query = select(func.count().over().label("total_count"), BudgetForm)
|
||||
@ -38,6 +38,11 @@ class BudgetFormRepository:
|
||||
if year is not None:
|
||||
where.append(BudgetForm.year == year)
|
||||
|
||||
if delete_status == "marked":
|
||||
where.append(BudgetForm.to_delete.is_(True))
|
||||
elif delete_status != "all":
|
||||
where.append(BudgetForm.to_delete.is_(False))
|
||||
|
||||
query = query.where(*where)
|
||||
if load_org:
|
||||
query = query.options(
|
||||
@ -63,19 +68,20 @@ class BudgetFormRepository:
|
||||
return (await self.db.execute(count_query)).scalar(), []
|
||||
else:
|
||||
return (await self.db.execute(query)).scalars().all()
|
||||
|
||||
|
||||
async def get(
|
||||
self,
|
||||
self,
|
||||
budget_form_id: int | None = None,
|
||||
org_unit: int | list[int] | None = None,
|
||||
form_type_code: FormTypeEnum | str | None = None,
|
||||
year: int | None = None,
|
||||
load_form_type: bool = False,
|
||||
load_org: bool = False,
|
||||
include_to_delete: bool = False,
|
||||
) -> BudgetForm | None:
|
||||
assert budget_form_id or all((form_type_code, year, org_unit))
|
||||
query = select(BudgetForm)
|
||||
|
||||
|
||||
where = []
|
||||
if budget_form_id is not None:
|
||||
where.append(BudgetForm.id == budget_form_id)
|
||||
@ -85,16 +91,19 @@ class BudgetFormRepository:
|
||||
where.append(BudgetForm.org_unit_id == org_unit)
|
||||
else:
|
||||
where.append(BudgetForm.org_unit_id.in_(org_unit))
|
||||
|
||||
|
||||
if form_type_code is not None:
|
||||
if isinstance(form_type_code, FormTypeEnum):
|
||||
where.append(BudgetForm.form_type_code == form_type_code.value)
|
||||
else:
|
||||
where.append(BudgetForm.form_type_code == form_type_code)
|
||||
|
||||
|
||||
if year is not None:
|
||||
where.append(BudgetForm.year == year)
|
||||
|
||||
if not include_to_delete:
|
||||
where.append(BudgetForm.to_delete.is_(False))
|
||||
|
||||
query = query.where(*where)
|
||||
if load_org:
|
||||
query = query.options(
|
||||
@ -106,12 +115,13 @@ class BudgetFormRepository:
|
||||
joinedload(BudgetForm.form_type)
|
||||
)
|
||||
return (await self.db.execute(query)).scalars().first()
|
||||
|
||||
|
||||
async def create(
|
||||
self,
|
||||
self,
|
||||
form_type_code: str,
|
||||
year: int,
|
||||
year: int,
|
||||
org_unit_id: int,
|
||||
user_id: int | None = None,
|
||||
) -> BudgetForm | None:
|
||||
form_id = (
|
||||
await self.db.execute(
|
||||
@ -134,6 +144,42 @@ class BudgetFormRepository:
|
||||
).scalar_one_or_none()
|
||||
if form_id is None:
|
||||
return None
|
||||
if user_id is not None:
|
||||
await self.db.execute(
|
||||
update(BudgetForm)
|
||||
.where(BudgetForm.id == form_id)
|
||||
.values(created_by=user_id, updated_by=user_id)
|
||||
)
|
||||
return await self.get(
|
||||
budget_form_id=form_id,
|
||||
)
|
||||
|
||||
async def mark_for_deletion(self, budget_form_id: int, user_id: int) -> None:
|
||||
await self.db.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE v3.budget_form
|
||||
SET to_delete = true, deleted_by = :user_id, deleted_at = now(), updated_by = :user_id, updated_at = now()
|
||||
WHERE id = :form_id
|
||||
"""
|
||||
),
|
||||
{"form_id": budget_form_id, "user_id": user_id},
|
||||
)
|
||||
|
||||
async def restore(self, budget_form_id: int, user_id: int) -> None:
|
||||
await self.db.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE v3.budget_form
|
||||
SET to_delete = false, deleted_by = NULL, deleted_at = NULL, updated_by = :user_id, updated_at = now()
|
||||
WHERE id = :form_id
|
||||
"""
|
||||
),
|
||||
{"form_id": budget_form_id, "user_id": user_id},
|
||||
)
|
||||
|
||||
async def hard_delete(self, budget_form_id: int) -> None:
|
||||
await self.db.execute(
|
||||
text("SELECT v3.hard_delete_budget_form(:form_id)"),
|
||||
{"form_id": budget_form_id},
|
||||
)
|
||||
|
||||
@ -4,12 +4,15 @@ from typing import Literal
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.repository.user_repository import UserRepository
|
||||
from src.core.errors import AccessDeniedException, ValidationException
|
||||
from src.db.models.app_user import AppUser
|
||||
from src.db.models.audit_log import AuditLog
|
||||
from src.db.models.budget_form import BudgetForm
|
||||
from src.db.models.role import UserRoleEnum
|
||||
from src.repository.budget_form_repository import BudgetFormRepository
|
||||
from src.repository.user_repository import UserRepository
|
||||
|
||||
DeleteStatus = Literal["active", "marked", "all"]
|
||||
|
||||
|
||||
class BudgetFormService:
|
||||
@ -28,8 +31,15 @@ class BudgetFormService:
|
||||
org_unit: list[int] | None = None,
|
||||
form_type: Literal["FORM_1", "FORM_2", "FORM_4"] | None = None,
|
||||
year: int | None = None,
|
||||
delete_status: DeleteStatus | None = None,
|
||||
) -> list[BudgetForm] | tuple[int, list[BudgetForm]]:
|
||||
if user.role_id == UserRoleEnum.ADMIN:
|
||||
if delete_status is not None and delete_status not in {"active", "marked", "all"}:
|
||||
raise ValidationException("delete_status должен быть active, marked или all")
|
||||
|
||||
is_admin = user.role_id == UserRoleEnum.ADMIN
|
||||
effective_delete_status = (delete_status or "all") if is_admin else "active"
|
||||
|
||||
if is_admin:
|
||||
return await self.bf_repo.get_list(
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
@ -38,6 +48,7 @@ class BudgetFormService:
|
||||
org_unit=org_unit,
|
||||
form_type=form_type,
|
||||
year=year,
|
||||
delete_status=effective_delete_status,
|
||||
)
|
||||
user = await self.user_repo.get(
|
||||
user_id=user.id,
|
||||
@ -58,6 +69,7 @@ class BudgetFormService:
|
||||
load_org=load_org,
|
||||
form_type=form_type,
|
||||
year=year,
|
||||
delete_status=effective_delete_status,
|
||||
)
|
||||
|
||||
async def get(
|
||||
@ -66,12 +78,15 @@ class BudgetFormService:
|
||||
user: AppUser,
|
||||
load_form_type: bool = False,
|
||||
load_org: bool = False,
|
||||
include_to_delete: bool = False,
|
||||
) -> BudgetForm | None:
|
||||
if user.role_id == UserRoleEnum.ADMIN:
|
||||
is_admin = user.role_id == UserRoleEnum.ADMIN
|
||||
if is_admin:
|
||||
return await self.bf_repo.get(
|
||||
budget_form_id=budget_form_id,
|
||||
load_form_type=load_form_type,
|
||||
load_org=load_org,
|
||||
include_to_delete=True,
|
||||
)
|
||||
user = await self.user_repo.get(
|
||||
user_id=user.id,
|
||||
@ -82,30 +97,35 @@ class BudgetFormService:
|
||||
org_unit=[ou.id for ou in user.org_units],
|
||||
load_form_type=load_form_type,
|
||||
load_org=load_org,
|
||||
include_to_delete=False,
|
||||
)
|
||||
|
||||
async def create(
|
||||
self,
|
||||
self,
|
||||
form_type_code: str,
|
||||
year: int,
|
||||
year: int,
|
||||
org_unit_id: int,
|
||||
user: AppUser | None = None,
|
||||
) -> BudgetForm | None:
|
||||
return await self.bf_repo.create(
|
||||
form_type_code=form_type_code,
|
||||
year=year,
|
||||
org_unit_id=org_unit_id,
|
||||
user_id=user.id if user else None,
|
||||
)
|
||||
|
||||
async def create_if_not_exists(
|
||||
self,
|
||||
self,
|
||||
form_type_code: str,
|
||||
year: int,
|
||||
year: int,
|
||||
org_unit_id: int,
|
||||
user: AppUser | None = None,
|
||||
) -> BudgetForm | None:
|
||||
existing_bf = await self.bf_repo.get(
|
||||
form_type_code=form_type_code,
|
||||
year=year,
|
||||
org_unit=org_unit_id,
|
||||
include_to_delete=True,
|
||||
)
|
||||
if existing_bf:
|
||||
return existing_bf
|
||||
@ -113,13 +133,15 @@ class BudgetFormService:
|
||||
form_type_code=form_type_code,
|
||||
year=year,
|
||||
org_unit_id=org_unit_id,
|
||||
user_id=user.id if user else None,
|
||||
)
|
||||
|
||||
|
||||
async def bulk_create(
|
||||
self,
|
||||
self,
|
||||
form_type_code: str,
|
||||
year: int,
|
||||
year: int,
|
||||
org_unit_ids: list[int],
|
||||
user: AppUser | None = None,
|
||||
) -> BudgetForm | None:
|
||||
return await asyncio.gather(
|
||||
*[
|
||||
@ -127,16 +149,18 @@ class BudgetFormService:
|
||||
form_type_code=form_type_code,
|
||||
year=year,
|
||||
org_unit_id=org_unit_id,
|
||||
user=user,
|
||||
)
|
||||
for org_unit_id in org_unit_ids
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def bulk_create_if_not_exists(
|
||||
self,
|
||||
self,
|
||||
form_type_code: str,
|
||||
year: int,
|
||||
year: int,
|
||||
org_unit_ids: list[int],
|
||||
user: AppUser | None = None,
|
||||
) -> BudgetForm | None:
|
||||
return await asyncio.gather(
|
||||
*[
|
||||
@ -144,7 +168,118 @@ class BudgetFormService:
|
||||
form_type_code=form_type_code,
|
||||
year=year,
|
||||
org_unit_id=org_unit_id,
|
||||
user=user,
|
||||
)
|
||||
for org_unit_id in org_unit_ids
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
async def mark_for_deletion(self, budget_form_id: int, user: AppUser) -> BudgetForm:
|
||||
self._ensure_admin(user)
|
||||
form = await self._get_admin_form(budget_form_id)
|
||||
if form.to_delete:
|
||||
return form
|
||||
await self.bf_repo.mark_for_deletion(budget_form_id=budget_form_id, user_id=user.id)
|
||||
await self.db.refresh(form)
|
||||
self._add_audit_log(
|
||||
event="FORM_MARKED_FOR_DELETION",
|
||||
user=user,
|
||||
form=form,
|
||||
event_data={
|
||||
"entity_type": "form",
|
||||
"entity_id": form.id,
|
||||
"form_id": form.id,
|
||||
"form_type_code": self._form_type_value(form.form_type_code),
|
||||
"year": form.year,
|
||||
"org_unit_id": form.org_unit_id,
|
||||
},
|
||||
)
|
||||
return form
|
||||
|
||||
async def restore(self, budget_form_id: int, user: AppUser) -> BudgetForm:
|
||||
self._ensure_admin(user)
|
||||
form = await self._get_admin_form(budget_form_id)
|
||||
if not form.to_delete:
|
||||
return form
|
||||
snapshot = self._form_snapshot(form)
|
||||
await self.bf_repo.restore(budget_form_id=budget_form_id, user_id=user.id)
|
||||
await self.db.refresh(form)
|
||||
self._add_audit_log(
|
||||
event="FORM_RESTORED",
|
||||
user=user,
|
||||
form=form,
|
||||
event_data={
|
||||
"entity_type": "form",
|
||||
"entity_id": form.id,
|
||||
"before": snapshot,
|
||||
},
|
||||
)
|
||||
return form
|
||||
|
||||
async def hard_delete(self, budget_form_id: int, user: AppUser) -> None:
|
||||
self._ensure_admin(user)
|
||||
form = await self._get_admin_form(budget_form_id)
|
||||
if not form.to_delete:
|
||||
raise ValidationException("Задача не помечена на удаление")
|
||||
if form.deleted_by == user.id:
|
||||
raise AccessDeniedException("Полностью удалить задачу должен другой администратор")
|
||||
snapshot = self._form_snapshot(form)
|
||||
self._add_audit_log(
|
||||
event="FORM_HARD_DELETED",
|
||||
user=user,
|
||||
form=form,
|
||||
event_data={
|
||||
"entity_type": "form",
|
||||
"entity_id": form.id,
|
||||
"before": snapshot,
|
||||
},
|
||||
)
|
||||
await self.db.flush()
|
||||
await self.bf_repo.hard_delete(budget_form_id=budget_form_id)
|
||||
|
||||
def _ensure_admin(self, user: AppUser) -> None:
|
||||
if user.role_id != UserRoleEnum.ADMIN:
|
||||
raise AccessDeniedException("Недостаточно прав")
|
||||
|
||||
async def _get_admin_form(self, budget_form_id: int) -> BudgetForm:
|
||||
form = await self.bf_repo.get(budget_form_id=budget_form_id, include_to_delete=True, load_org=True)
|
||||
if not form:
|
||||
raise ValidationException("Форма не найдена")
|
||||
return form
|
||||
|
||||
def _add_audit_log(
|
||||
self,
|
||||
event: str,
|
||||
user: AppUser,
|
||||
form: BudgetForm,
|
||||
event_data: dict,
|
||||
) -> None:
|
||||
self.db.add(
|
||||
AuditLog(
|
||||
user_id=user.id,
|
||||
org_unit_id=form.org_unit_id,
|
||||
form_id=form.id,
|
||||
task_id=form.id,
|
||||
event=event,
|
||||
event_type="FORM",
|
||||
event_data=event_data,
|
||||
)
|
||||
)
|
||||
|
||||
def _form_type_value(self, form_type_code) -> str:
|
||||
return getattr(form_type_code, "value", str(form_type_code))
|
||||
|
||||
def _form_snapshot(self, form: BudgetForm) -> dict:
|
||||
return {
|
||||
"id": form.id,
|
||||
"form_type_code": self._form_type_value(form.form_type_code),
|
||||
"year": form.year,
|
||||
"org_unit_id": form.org_unit_id,
|
||||
"created_by": form.created_by,
|
||||
"created_at": form.created_at.isoformat() if form.created_at else None,
|
||||
"updated_by": form.updated_by,
|
||||
"updated_at": form.updated_at.isoformat() if form.updated_at else None,
|
||||
"to_delete": form.to_delete,
|
||||
"deleted_by": form.deleted_by,
|
||||
"deleted_at": form.deleted_at.isoformat() if form.deleted_at else None,
|
||||
}
|
||||
|
||||
@ -31,6 +31,15 @@ export const TasksApi = {
|
||||
responseType: 'blob',
|
||||
});
|
||||
},
|
||||
markForDeletion(taskId) {
|
||||
return api.post(`/form/${taskId}/mark-delete`).then((r) => r.data);
|
||||
},
|
||||
restore(taskId) {
|
||||
return api.post(`/form/${taskId}/restore`).then((r) => r.data);
|
||||
},
|
||||
hardDelete(taskId) {
|
||||
return api.delete(`/form/${taskId}/hard`).then((r) => r.data);
|
||||
},
|
||||
exportTasks(payload, params = {}) {
|
||||
return api.post(`export/bulk`, payload, {
|
||||
params,
|
||||
|
||||
@ -681,6 +681,10 @@ export const config = {
|
||||
color_type: yellowColumn,
|
||||
accessorKey: "data.q4.adj_reserve",
|
||||
},
|
||||
"data.q4.adj_comment": {
|
||||
color_type: yellowColumn,
|
||||
accessorKey: "data.q4.adj_comment",
|
||||
},
|
||||
"data.q4.corrected_plan": {
|
||||
color_type: yellowColumn,
|
||||
accessorKey: "data.q4.corrected_plan",
|
||||
@ -3663,6 +3667,25 @@ export const config = {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Комментарий",
|
||||
accessorKey: "4_kvartal_2026_adj_kommentariy",
|
||||
columns: [
|
||||
{
|
||||
header: "Сумма (без НДС)",
|
||||
accessorKey: "data.q4.adj_comment",
|
||||
columnLetter: "GG",
|
||||
size: 150,
|
||||
filterFn: "contains",
|
||||
},
|
||||
],
|
||||
muiTableHeadCellProps: {
|
||||
sx: {
|
||||
backgroundColor: "#FFFF99",
|
||||
color: "#000000",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Скорректированный план 4 кв. {year:short}",
|
||||
accessorKey: "4_kvartal_2026_skorr_plan_4_kv_26",
|
||||
|
||||
@ -681,6 +681,10 @@ export const config = {
|
||||
color_type: yellowColumn,
|
||||
accessorKey: "data.q4.adj_reserve",
|
||||
},
|
||||
"data.q4.adj_comment": {
|
||||
color_type: yellowColumn,
|
||||
accessorKey: "data.q4.adj_comment",
|
||||
},
|
||||
"data.q4.corrected_plan": {
|
||||
color_type: yellowColumn,
|
||||
accessorKey: "data.q4.corrected_plan",
|
||||
@ -3659,6 +3663,25 @@ export const config = {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Комментарий",
|
||||
accessorKey: "4_kvartal_2026_adj_kommentariy",
|
||||
columns: [
|
||||
{
|
||||
header: "Сумма (без НДС)",
|
||||
accessorKey: "data.q4.adj_comment",
|
||||
columnLetter: "GG",
|
||||
size: 150,
|
||||
filterFn: "contains",
|
||||
},
|
||||
],
|
||||
muiTableHeadCellProps: {
|
||||
sx: {
|
||||
backgroundColor: "#FFFF99",
|
||||
color: "#000000",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Скорректированный план 4 кв. {year:short}",
|
||||
accessorKey: "4_kvartal_2026_skorr_plan_4_kv_26",
|
||||
|
||||
@ -682,6 +682,10 @@ export const config = {
|
||||
color_type: yellowColumn,
|
||||
accessorKey: "data.q4.adj_reserve",
|
||||
},
|
||||
"data.q4.adj_comment": {
|
||||
color_type: yellowColumn,
|
||||
accessorKey: "data.q4.adj_comment",
|
||||
},
|
||||
"data.q4.corrected_plan": {
|
||||
color_type: yellowColumn,
|
||||
accessorKey: "data.q4.corrected_plan",
|
||||
@ -3659,6 +3663,25 @@ export const config = {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Комментарий",
|
||||
accessorKey: "4_kvartal_2026_adj_kommentariy",
|
||||
columns: [
|
||||
{
|
||||
header: "Сумма (без НДС)",
|
||||
accessorKey: "data.q4.adj_comment",
|
||||
columnLetter: "GG",
|
||||
size: 150,
|
||||
filterFn: "contains",
|
||||
},
|
||||
],
|
||||
muiTableHeadCellProps: {
|
||||
sx: {
|
||||
backgroundColor: "#FFFF99",
|
||||
color: "#000000",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Скорректированный план 4 кв. {year:short}",
|
||||
accessorKey: "4_kvartal_2026_skorr_plan_4_kv_26",
|
||||
|
||||
@ -3571,13 +3571,6 @@ export const config = {
|
||||
size: 260,
|
||||
filterFn: "contains",
|
||||
},
|
||||
{
|
||||
header: "Сумма, тыс. руб. (без НДС)",
|
||||
accessorKey: "data.q4.adj_comment",
|
||||
columnLetter: "GG",
|
||||
size: 250,
|
||||
filterFn: "contains",
|
||||
},
|
||||
],
|
||||
muiTableHeadCellProps: {
|
||||
sx: {
|
||||
@ -3688,6 +3681,25 @@ export const config = {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Комментарий",
|
||||
accessorKey: "4_kvartal_2026_adj_kommentariy",
|
||||
columns: [
|
||||
{
|
||||
header: "Сумма, тыс. руб. (без НДС)",
|
||||
accessorKey: "data.q4.adj_comment",
|
||||
columnLetter: "GG",
|
||||
size: 250,
|
||||
filterFn: "contains",
|
||||
},
|
||||
],
|
||||
muiTableHeadCellProps: {
|
||||
sx: {
|
||||
backgroundColor: "#FFFF66",
|
||||
color: "#000000",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Скорректированный план 4 кв. {year:short}",
|
||||
accessorKey: "4_kvartal_2026_skorr_plan_4_kv_26",
|
||||
|
||||
@ -59,6 +59,9 @@ export const TaskCardComponent = ({
|
||||
onAddForExport,
|
||||
onExportClick,
|
||||
isLastOpened = false,
|
||||
isAdmin = false,
|
||||
onChanged,
|
||||
currentUserId = null,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
@ -67,11 +70,14 @@ export const TaskCardComponent = ({
|
||||
if (isLastOpened) cardRef.current?.scrollIntoView({ block: 'nearest' });
|
||||
}, [isLastOpened]);
|
||||
const { id, title, org_unit, form_type_code, year } = taskForm;
|
||||
const canHardDelete = taskForm.to_delete && taskForm.deleted_by !== currentUserId;
|
||||
|
||||
const [isEditModalOpen, setEditModalOpen] = useState(false);
|
||||
const [editedTitle, setEditedTitle] = useState('');
|
||||
const [currentTypeCode, setCurrentTypeCode] = useState(FORM_TYPE_TRANSLATE[form_type_code] || '');
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
const [confirmAction, setConfirmAction] = useState(null);
|
||||
const [actionLoading, setActionLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsChecked(false);
|
||||
@ -102,6 +108,52 @@ export const TaskCardComponent = ({
|
||||
onExportClick?.(id, title);
|
||||
};
|
||||
|
||||
const openConfirmAction = (event, action) => {
|
||||
event.stopPropagation();
|
||||
setConfirmAction(action);
|
||||
};
|
||||
|
||||
const closeConfirmAction = () => {
|
||||
if (!actionLoading) setConfirmAction(null);
|
||||
};
|
||||
|
||||
const runConfirmedAction = async () => {
|
||||
if (!confirmAction) return;
|
||||
try {
|
||||
setActionLoading(true);
|
||||
if (confirmAction === 'mark') {
|
||||
await TasksApi.markForDeletion(id);
|
||||
toast.success('Задача помечена на удаление');
|
||||
}
|
||||
if (confirmAction === 'restore') {
|
||||
await TasksApi.restore(id);
|
||||
toast.success('Задача возвращена');
|
||||
}
|
||||
if (confirmAction === 'hardDelete') {
|
||||
await TasksApi.hardDelete(id);
|
||||
toast.success('Задача полностью удалена');
|
||||
}
|
||||
setConfirmAction(null);
|
||||
onChanged?.();
|
||||
} catch (error) {
|
||||
toast.error(error?.response?.data?.detail || 'Не удалось выполнить действие');
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const actionTitle = {
|
||||
mark: 'Удалить задачу',
|
||||
restore: 'Вернуть задачу',
|
||||
hardDelete: 'Полностью удалить задачу',
|
||||
}[confirmAction];
|
||||
|
||||
const actionText = {
|
||||
mark: 'Задача будет скрыта от пользователей. Админы смогут вернуть её или удалить окончательно.',
|
||||
restore: 'Задача снова станет доступна пользователям.',
|
||||
hardDelete: 'Будут удалены задача и все связанные данные. Это действие нельзя отменить.',
|
||||
}[confirmAction];
|
||||
|
||||
const updateTask = async (nextTitle) => {
|
||||
try {
|
||||
await TasksApi.editTask(id, { title: nextTitle });
|
||||
@ -147,6 +199,7 @@ export const TaskCardComponent = ({
|
||||
<TaskCard
|
||||
ref={cardRef}
|
||||
$fluidWidth={fluidWidth}
|
||||
$marked={Boolean(taskForm.to_delete)}
|
||||
onClick={(event) => {
|
||||
if (exportMode) {
|
||||
handleCheckboxClick(event);
|
||||
@ -159,7 +212,10 @@ export const TaskCardComponent = ({
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<CardHeader>
|
||||
<TaskId>ID: {id}</TaskId>
|
||||
{isLastOpened && <LastOpenedBadge sx={{ mb: 0 }}>Последняя открытая</LastOpenedBadge>}
|
||||
<Stack direction='row' gap={0.75} alignItems='center'>
|
||||
{taskForm.to_delete && <Chip sx={{ background: '#fde8e8', color: '#b42318' }}>На удаление</Chip>}
|
||||
{isLastOpened && <LastOpenedBadge sx={{ mb: 0 }}>Последняя открытая</LastOpenedBadge>}
|
||||
</Stack>
|
||||
</CardHeader>
|
||||
|
||||
<TaskTitle>{org_unit?.title || 'Подразделение не указано'}</TaskTitle>
|
||||
@ -182,10 +238,47 @@ export const TaskCardComponent = ({
|
||||
|
||||
</CardBody>
|
||||
|
||||
{!isForm && !exportMode && <ExportButton onClick={handleExportTask} />}
|
||||
{exportMode && <div>{isChecked ? <CheckBoxCheckSvg size='1.625rem' /> : <CheckBoxUncheckSvg size='1.625rem' />}</div>}
|
||||
<Stack direction='row' gap={0.75} alignItems='center' onClick={(event) => event.stopPropagation()}>
|
||||
{isAdmin && !isForm && !exportMode && !taskForm.to_delete && (
|
||||
<Button size='small' variant='text' color='error' onClick={(event) => openConfirmAction(event, 'mark')}>
|
||||
Удалить
|
||||
</Button>
|
||||
)}
|
||||
{isAdmin && !isForm && !exportMode && taskForm.to_delete && (
|
||||
<>
|
||||
<Button size='small' variant='text' onClick={(event) => openConfirmAction(event, 'restore')}>
|
||||
Вернуть
|
||||
</Button>
|
||||
{canHardDelete && (
|
||||
<Button size='small' variant='text' color='error' onClick={(event) => openConfirmAction(event, 'hardDelete')}>
|
||||
Удалить
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!isForm && !exportMode && !taskForm.to_delete && <ExportButton onClick={handleExportTask} />}
|
||||
{exportMode && <div>{isChecked ? <CheckBoxCheckSvg size='1.625rem' /> : <CheckBoxUncheckSvg size='1.625rem' />}</div>}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</TaskCard>
|
||||
|
||||
<Modal
|
||||
open={Boolean(confirmAction)}
|
||||
onClose={closeConfirmAction}
|
||||
title={actionTitle}
|
||||
size='small'
|
||||
actions={
|
||||
<>
|
||||
<Button variant='grey' onClick={closeConfirmAction} disabled={actionLoading}>
|
||||
Отменить
|
||||
</Button>
|
||||
<Button variant='contained' color={confirmAction === 'hardDelete' || confirmAction === 'mark' ? 'error' : 'primary'} onClick={runConfirmedAction} disabled={actionLoading}>
|
||||
Подтвердить
|
||||
</Button>
|
||||
</>
|
||||
}>
|
||||
{actionText}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -10,12 +10,13 @@ export const TaskCard = styled.div`
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
box-sizing: border-box;
|
||||
border: 0.06rem solid var(--Stroke, rgba(0, 0, 0, 0.1));
|
||||
border: 0.06rem solid ${({ $marked }) => ($marked ? 'rgba(211, 47, 47, 0.35)' : 'var(--Stroke, rgba(0, 0, 0, 0.1))')};
|
||||
border-radius: 1rem;
|
||||
box-shadow:
|
||||
0rem 2rem 4rem -2rem rgba(0, 0, 0, 0.02),
|
||||
0rem 4rem 6rem -1rem rgba(0, 0, 0, 0.02);
|
||||
background: var(--White, rgba(255, 255, 255, 1));
|
||||
background: ${({ $marked }) => ($marked ? '#fff7f7' : 'var(--White, rgba(255, 255, 255, 1))')};
|
||||
opacity: ${({ $marked }) => ($marked ? 0.72 : 1)};
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
@ -5,6 +5,9 @@ export const DEFAULT_ACTIONS = {
|
||||
ACCESS_WINDOW_CHANGE: 'Изменение этапа',
|
||||
USER_ROLE_CHANGE: 'Изменение роли пользователя',
|
||||
CREATE_TASK: 'Создание задачи',
|
||||
FORM_MARKED_FOR_DELETION: 'Задача отмечена на удаление',
|
||||
FORM_RESTORED: 'Задача возвращена',
|
||||
FORM_HARD_DELETED: 'Задача полностью удалена',
|
||||
// UPDATE_TASK: 'Обновление задачи', //сейчас нет события
|
||||
// DELETE_TASK: 'Удаление задачи',
|
||||
USER_CREATE: 'Создание пользователя',
|
||||
|
||||
@ -58,6 +58,21 @@ export const EVENT_CONFIGS = {
|
||||
form_type: 'Форма',
|
||||
ssp_list: 'Создана для СПП/РФ',
|
||||
},
|
||||
FORM_MARKED_FOR_DELETION: {
|
||||
entity_id: 'Задача',
|
||||
form_id: 'Задача',
|
||||
form_type_code: 'Форма',
|
||||
year: 'Год',
|
||||
org_unit_id: 'ССП/РФ',
|
||||
},
|
||||
FORM_RESTORED: {
|
||||
entity_id: 'Задача',
|
||||
before: 'Данные до возврата',
|
||||
},
|
||||
FORM_HARD_DELETED: {
|
||||
entity_id: 'Задача',
|
||||
before: 'Удалённые данные',
|
||||
},
|
||||
ROW_CREATE: {
|
||||
sheet: 'Лист',
|
||||
expense_item_id: 'Статья расходов',
|
||||
|
||||
@ -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}`,
|
||||
|
||||
@ -39,6 +39,7 @@ export default function TasksPage() {
|
||||
const yearFilter = searchParams.get('year') || '';
|
||||
const formTypeFilter = searchParams.get('form_type') || '';
|
||||
const orgUnitsFilter = searchParams.get('org_units') || '';
|
||||
const deleteStatusFilter = searchParams.get('delete_status') || 'all';
|
||||
const orgUnitIdsFilter = useMemo(
|
||||
() =>
|
||||
orgUnitsFilter
|
||||
@ -185,6 +186,7 @@ export default function TasksPage() {
|
||||
...(yearFilter && { year: Number(yearFilter) }),
|
||||
...(formTypeFilter && { form_type: formTypeFilter }),
|
||||
...(orgUnitIdsFilter.length > 0 && { org_units: orgUnitIdsFilter.join(',') }),
|
||||
...(user?.role_id === ROLES_NAME_ID.admin && deleteStatusFilter && { delete_status: deleteStatusFilter }),
|
||||
};
|
||||
|
||||
const res = await TasksApi.list(params);
|
||||
@ -219,7 +221,7 @@ export default function TasksPage() {
|
||||
if (currentRequest === requestId.current) setLoading(false);
|
||||
}
|
||||
},
|
||||
[yearFilter, formTypeFilter, orgUnitIdsFilter, setSearchParams],
|
||||
[yearFilter, formTypeFilter, orgUnitIdsFilter, deleteStatusFilter, user?.role_id, setSearchParams],
|
||||
);
|
||||
|
||||
const handlePageChange = (_event, value) => {
|
||||
@ -266,6 +268,7 @@ export default function TasksPage() {
|
||||
nextParams.delete('year');
|
||||
nextParams.delete('form_type');
|
||||
nextParams.delete('org_units');
|
||||
nextParams.delete('delete_status');
|
||||
return nextParams;
|
||||
});
|
||||
};
|
||||
@ -337,6 +340,19 @@ export default function TasksPage() {
|
||||
sx={{ width: 480, maxWidth: '100%' }}
|
||||
/>
|
||||
|
||||
{user.role_id === ROLES_NAME_ID.admin && (
|
||||
<Select
|
||||
size='small'
|
||||
displayEmpty
|
||||
value={deleteStatusFilter}
|
||||
onChange={(event) => handleFilterChange('delete_status', event.target.value)}
|
||||
sx={{ width: 230 }}>
|
||||
<MenuItem value='active'>Непомеченные</MenuItem>
|
||||
<MenuItem value='marked'>Помеченные</MenuItem>
|
||||
<MenuItem value='all'>Все задачи</MenuItem>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
<ResetFiltersButton onClick={handleClearFilters} />
|
||||
</Box>
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user