projects-handlers: ручки со списком проектов со сметами и их обновление #90
82
api/alembic/versions/0016_project_fields.py
Normal file
82
api/alembic/versions/0016_project_fields.py
Normal file
@ -0,0 +1,82 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision = "0016"
|
||||
down_revision = "0015"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
_DOLLAR_TAG_RE = re.compile(r"\$\w+\$")
|
||||
|
||||
|
||||
def _find_dollar_tag(line: str) -> str | None:
|
||||
m = _DOLLAR_TAG_RE.search(line.strip())
|
||||
return m.group(0) if m else None
|
||||
|
||||
|
||||
def _split_statements(sql: str) -> list[str]:
|
||||
statements: list[str] = []
|
||||
current: list[str] = []
|
||||
in_dollar = False
|
||||
dollar_tag: str | None = None
|
||||
|
||||
for line in sql.split("\n"):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("--"):
|
||||
continue
|
||||
|
||||
if not in_dollar:
|
||||
tag = _find_dollar_tag(stripped)
|
||||
if tag and tag.endswith("$") and tag.startswith("$"):
|
||||
dollar_tag = tag
|
||||
in_dollar = True
|
||||
current.append(line)
|
||||
continue
|
||||
|
||||
if in_dollar and dollar_tag and stripped.startswith(dollar_tag):
|
||||
after = stripped[len(dollar_tag):].strip()
|
||||
if after == ";" or after == "":
|
||||
in_dollar = False
|
||||
dollar_tag = None
|
||||
if after == ";":
|
||||
current.append(line)
|
||||
statements.append("\n".join(current))
|
||||
current = []
|
||||
continue
|
||||
|
||||
if not in_dollar and stripped.rstrip().endswith(";"):
|
||||
current.append(line)
|
||||
statements.append("\n".join(current))
|
||||
current = []
|
||||
continue
|
||||
|
||||
current.append(line)
|
||||
|
||||
remaining = "\n".join(current).strip()
|
||||
if remaining:
|
||||
statements.append(remaining)
|
||||
|
||||
return statements
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
ddl_path = os.path.join(os.path.dirname(__file__), "sql", "0016_project_fields.sql")
|
||||
with open(ddl_path) as f:
|
||||
content = f.read()
|
||||
|
||||
statements = _split_statements(content)
|
||||
for stmt in statements:
|
||||
stripped = stmt.strip().rstrip(";").strip()
|
||||
if not stripped:
|
||||
continue
|
||||
if all(l.strip().startswith("--") or not l.strip() for l in stripped.split("\n")):
|
||||
continue
|
||||
op.execute(stripped)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
940
api/alembic/versions/sql/0016_project_fields.sql
Normal file
940
api/alembic/versions/sql/0016_project_fields.sql
Normal file
@ -0,0 +1,940 @@
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- МИГРАЦИЯ: Поля project + реструктуризация rf_project_report
|
||||
-- (year -> rf_project_year, атрибуты сметы -> smeta)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Поля project
|
||||
ALTER TABLE v3.project ADD COLUMN status VARCHAR DEFAULT 'created';
|
||||
ALTER TABLE v3.project ADD COLUMN technical_number VARCHAR;
|
||||
ALTER TABLE v3.project ADD COLUMN krf_decision_date DATE;
|
||||
ALTER TABLE v3.project ADD COLUMN fk_decision_date DATE;
|
||||
ALTER TABLE v3.project ADD COLUMN board_decision_date DATE;
|
||||
ALTER TABLE v3.project ADD COLUMN open_relocate_close_date DATE;
|
||||
ALTER TABLE v3.project ADD COLUMN funding_by_ko_decision VARCHAR;
|
||||
|
||||
ALTER TABLE v3.project DROP CONSTRAINT IF EXISTS project_status_check;
|
||||
ALTER TABLE v3.project ADD CONSTRAINT project_status_check
|
||||
CHECK (status IS NULL OR status IN ('created', 'agreed', 'archived'));
|
||||
|
||||
ALTER TABLE v3.project DROP CONSTRAINT IF EXISTS project_funding_ko_check;
|
||||
ALTER TABLE v3.project ADD CONSTRAINT project_funding_ko_check
|
||||
CHECK (funding_by_ko_decision IS NULL OR funding_by_ko_decision IN ('prrs_budget', 'bank_reserve'));
|
||||
|
||||
-- rf_project_year: год проекта как отдельная сущность
|
||||
CREATE TABLE v3.rf_project_year (
|
||||
id SERIAL PRIMARY KEY,
|
||||
project_id INTEGER NOT NULL REFERENCES v3.project(id),
|
||||
year INTEGER NOT NULL,
|
||||
CONSTRAINT uq_v3_rf_project_year UNIQUE (project_id, year)
|
||||
);
|
||||
CREATE INDEX ix_v3_rf_project_year_project ON v3.rf_project_year(project_id);
|
||||
|
||||
-- Перенос существующих year из rf_project_report в rf_project_year
|
||||
INSERT INTO v3.rf_project_year (project_id, year)
|
||||
SELECT DISTINCT project_id, year FROM v3.rf_project_report;
|
||||
|
||||
-- Связь rf_project_report с rf_project_year
|
||||
ALTER TABLE v3.rf_project_report ADD COLUMN rf_project_year_id INTEGER;
|
||||
|
||||
UPDATE v3.rf_project_report rpr
|
||||
SET rf_project_year_id = rpy.id
|
||||
FROM v3.rf_project_year rpy
|
||||
WHERE rpy.project_id = rpr.project_id AND rpy.year = rpr.year;
|
||||
|
||||
ALTER TABLE v3.rf_project_report ALTER COLUMN rf_project_year_id SET NOT NULL;
|
||||
ALTER TABLE v3.rf_project_report
|
||||
ADD CONSTRAINT fk_v3_rf_project_report_year
|
||||
FOREIGN KEY (rf_project_year_id) REFERENCES v3.rf_project_year(id);
|
||||
|
||||
-- Убираем year из rf_project_report
|
||||
ALTER TABLE v3.rf_project_report DROP CONSTRAINT IF EXISTS uq_v3_rf_project_report;
|
||||
DROP INDEX IF EXISTS v3.ix_v3_rf_project_report_proj_year;
|
||||
ALTER TABLE v3.rf_project_report DROP COLUMN year;
|
||||
|
||||
-- Восстанавливаем уникальность и индекс на новой связке
|
||||
ALTER TABLE v3.rf_project_report
|
||||
ADD CONSTRAINT uq_v3_rf_project_report UNIQUE (project_id, rf_project_year_id, report_type);
|
||||
CREATE INDEX ix_v3_rf_project_report_proj_year ON v3.rf_project_report(project_id, rf_project_year_id);
|
||||
|
||||
-- smeta: атрибуты сметы на год проекта
|
||||
CREATE TABLE v3.smeta (
|
||||
id SERIAL PRIMARY KEY,
|
||||
rf_project_year_id INTEGER NOT NULL REFERENCES v3.rf_project_year(id),
|
||||
is_in_plan BOOLEAN,
|
||||
is_in_plan_q2 BOOLEAN,
|
||||
is_in_plan_q3 BOOLEAN,
|
||||
is_in_plan_q4 BOOLEAN,
|
||||
development_block VARCHAR,
|
||||
reserve_to_prrs_ahr NUMERIC,
|
||||
reserve_to_prrs_kv NUMERIC,
|
||||
CONSTRAINT smeta_development_block_check
|
||||
CHECK (development_block IS NULL OR development_block IN ('out_of_base_plan', 'carryover', 'additional_funding'))
|
||||
);
|
||||
|
||||
|
||||
-- Funcs
|
||||
CREATE OR REPLACE FUNCTION v3.add_form3_phase(p_rf_project_report_id integer, p_phase_code character varying, p_role character varying, p_column_keys text[], p_opens_at timestamp without time zone, p_closes_at timestamp without time zone)
|
||||
RETURNS v3.form3_phase
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_phase v3.form3_phase;
|
||||
v_org_unit_id INT;
|
||||
v_org_unit_title VARCHAR;
|
||||
v_year INT;
|
||||
v_task_name TEXT;
|
||||
v_phase_id TEXT;
|
||||
v_utc_offset VARCHAR(3);
|
||||
v_report_type VARCHAR;
|
||||
v_project_id INT;
|
||||
BEGIN
|
||||
SELECT p.org_unit_id, ou.title, rpry.year, ou.utc_offset, rpr.report_type, rpr.project_id
|
||||
INTO v_org_unit_id, v_org_unit_title, v_year, v_utc_offset, v_report_type, v_project_id
|
||||
FROM v3.rf_project_report rpr
|
||||
JOIN v3.rf_project_year rpry ON rpr.rf_project_year_id = rpry.id
|
||||
LEFT JOIN v3.project p ON p.id = rpr.project_id
|
||||
LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id
|
||||
WHERE rpr.id = p_rf_project_report_id;
|
||||
|
||||
INSERT INTO v3.form3_phase
|
||||
(rf_project_report_id, phase_code, role, column_keys, opens_at, closes_at)
|
||||
VALUES (
|
||||
p_rf_project_report_id,
|
||||
p_phase_code,
|
||||
p_role,
|
||||
p_column_keys,
|
||||
CASE WHEN p_opens_at IS NOT NULL THEN (p_opens_at::text || v_utc_offset)::timestamptz ELSE NULL END,
|
||||
CASE WHEN p_closes_at IS NOT NULL THEN (p_closes_at::text || v_utc_offset)::timestamptz ELSE NULL END
|
||||
|
||||
)
|
||||
RETURNING * INTO v_phase;
|
||||
|
||||
|
||||
v_task_name := trim(concat(
|
||||
'FORM_3',
|
||||
CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
|
||||
CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
|
||||
));
|
||||
v_phase_id := format('%s:%s', p_rf_project_report_id, p_phase_code);
|
||||
|
||||
PERFORM v3.log_event(
|
||||
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
||||
jsonb_build_object(
|
||||
'entity_type', 'project',
|
||||
'entity_id', v_project_id,
|
||||
'core_entity_type', 'form3_phase',
|
||||
'core_entity_id', v_phase_id,
|
||||
'action', 'create',
|
||||
'phase_id', v_phase_id,
|
||||
'task_name', NULLIF(v_task_name, ''),
|
||||
'report_id', p_rf_project_report_id,
|
||||
'org_unit_id', v_org_unit_id,
|
||||
'org_unit_name', v_org_unit_title,
|
||||
'year', v_year,
|
||||
'project_id', v_project_id,
|
||||
'report_type', v_report_type,
|
||||
'phase_code', p_phase_code,
|
||||
'role', v_phase.role,
|
||||
'column_keys', v_phase.column_keys,
|
||||
'opens_at', v_phase.opens_at,
|
||||
'closes_at', v_phase.closes_at
|
||||
),
|
||||
null, null, v_org_unit_id
|
||||
);
|
||||
|
||||
RETURN v_phase;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
|
||||
-- DROP FUNCTION v3.add_project_year(int4, int4, bool);
|
||||
|
||||
CREATE OR REPLACE FUNCTION v3.add_project_year(p_project_id integer, p_year integer, p_emit_audit boolean DEFAULT true)
|
||||
RETURNS TABLE(limit_report_id integer, current_expenses_report_id integer)
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_ryid INT;
|
||||
v_lim INT;
|
||||
v_cur INT;
|
||||
BEGIN
|
||||
IF p_year IS NULL THEN
|
||||
RAISE EXCEPTION 'p_year обязателен';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_project_id) THEN
|
||||
RAISE EXCEPTION 'project id=% не существует', p_project_id;
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM v3.rf_project_year
|
||||
WHERE project_id = p_project_id AND year = p_year) THEN
|
||||
RAISE EXCEPTION 'год % у проекта id=% уже заведён', p_year, p_project_id;
|
||||
END IF;
|
||||
|
||||
INSERT INTO v3.rf_project_year (project_id, year)
|
||||
VALUES (p_project_id, p_year)
|
||||
RETURNING id INTO v_ryid;
|
||||
|
||||
INSERT INTO v3.smeta (rf_project_year_id)
|
||||
VALUES (v_ryid);
|
||||
|
||||
INSERT INTO v3.rf_project_report (project_id, rf_project_year_id, report_type)
|
||||
VALUES (p_project_id, v_ryid, 'LIMIT') RETURNING id INTO v_lim;
|
||||
INSERT INTO v3.rf_project_report (project_id, rf_project_year_id, report_type)
|
||||
VALUES (p_project_id, v_ryid, 'CURRENT_EXPENSES') RETURNING id INTO v_cur;
|
||||
|
||||
PERFORM v3.copy_template_to_report(v_lim);
|
||||
PERFORM v3.copy_template_to_report(v_cur);
|
||||
|
||||
PERFORM v3.seed_form3_report_lines(v_lim);
|
||||
PERFORM v3.seed_form3_report_lines(v_cur);
|
||||
|
||||
IF p_emit_audit THEN
|
||||
PERFORM v3.log_event(
|
||||
'PROJECT_YEAR_ADD', 'PROJECT',
|
||||
jsonb_build_object(
|
||||
'project_id', p_project_id,
|
||||
'year', p_year,
|
||||
'limit_report_id', v_lim,
|
||||
'current_expenses_report_id', v_cur
|
||||
)
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN QUERY SELECT v_lim, v_cur;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
-- DROP FUNCTION v3.del_form3_phase(int4, varchar);
|
||||
|
||||
CREATE OR REPLACE FUNCTION v3.del_form3_phase(p_rf_project_report_id integer, p_phase_code character varying)
|
||||
RETURNS boolean
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_old v3.form3_phase;
|
||||
v_org_unit_id INT;
|
||||
v_org_unit_title VARCHAR;
|
||||
v_year INT;
|
||||
v_task_name TEXT;
|
||||
v_phase_id TEXT;
|
||||
v_report_type VARCHAR;
|
||||
v_project_id INT;
|
||||
BEGIN
|
||||
DELETE FROM v3.form3_phase
|
||||
WHERE rf_project_report_id = p_rf_project_report_id
|
||||
AND phase_code = p_phase_code
|
||||
RETURNING * INTO v_old;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
|
||||
SELECT p.org_unit_id, ou.title, rpry.year, rpr.report_type, rpr.project_id
|
||||
INTO v_org_unit_id, v_org_unit_title, v_year, v_report_type, v_project_id
|
||||
FROM v3.rf_project_report rpr
|
||||
JOIN v3.rf_project_year rpry ON rpr.rf_project_year_id = rpry.id
|
||||
LEFT JOIN v3.project p ON p.id = rpr.project_id
|
||||
LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id
|
||||
WHERE rpr.id = p_rf_project_report_id;
|
||||
v_task_name := trim(concat(
|
||||
'FORM_3',
|
||||
CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
|
||||
CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
|
||||
));
|
||||
v_phase_id := format('%s:%s', p_rf_project_report_id, p_phase_code);
|
||||
|
||||
PERFORM v3.log_event(
|
||||
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
||||
jsonb_build_object(
|
||||
'entity_type', 'project',
|
||||
'entity_id', v_project_id,
|
||||
'core_entity_type', 'form3_phase',
|
||||
'core_entity_id', v_phase_id,
|
||||
'action', 'delete',
|
||||
'phase_id', v_phase_id,
|
||||
'phase_name', p_phase_code,
|
||||
'task_name', NULLIF(v_task_name, ''),
|
||||
'project_id', v_project_id,
|
||||
'year', v_year,
|
||||
'report_type', v_report_type,
|
||||
'org_unit_id', v_org_unit_id,
|
||||
'org_unit_name', v_org_unit_title,
|
||||
'phase_code', p_phase_code,
|
||||
'role', v_old.role,
|
||||
'column_keys', v_old.column_keys,
|
||||
'opens_at', v_old.opens_at,
|
||||
'closes_at', v_old.closes_at
|
||||
),
|
||||
null, null, v_org_unit_id
|
||||
);
|
||||
RETURN TRUE;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
-- DROP FUNCTION v3.upd_form3_phase(int4, varchar, varchar, _text, timestamp, timestamp);
|
||||
|
||||
CREATE OR REPLACE FUNCTION v3.upd_form3_phase(p_rf_project_report_id integer, p_phase_code character varying, p_role character varying DEFAULT NULL::character varying, p_column_keys text[] DEFAULT NULL::text[], p_opens_at timestamp without time zone DEFAULT NULL::timestamp without time zone, p_closes_at timestamp without time zone DEFAULT NULL::timestamp without time zone)
|
||||
RETURNS v3.form3_phase
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_old v3.form3_phase;
|
||||
v_new v3.form3_phase;
|
||||
v_is_extend BOOLEAN := FALSE;
|
||||
v_has_non_extend_change BOOLEAN := FALSE;
|
||||
v_has_window_change BOOLEAN := FALSE;
|
||||
v_changes JSONB := '{}'::jsonb;
|
||||
v_org_unit_id INT;
|
||||
v_org_unit_title VARCHAR;
|
||||
v_form_type_code VARCHAR;
|
||||
v_year INT;
|
||||
v_task_name TEXT;
|
||||
v_phase_id TEXT;
|
||||
v_utc_offset VARCHAR(3);
|
||||
v_opens_at timestamptz;
|
||||
v_closes_at timestamptz;
|
||||
v_report_type VARCHAR;
|
||||
v_project_id INT;
|
||||
BEGIN
|
||||
SELECT * INTO v_old
|
||||
FROM v3.form3_phase
|
||||
WHERE rf_project_report_id = p_rf_project_report_id
|
||||
AND phase_code = p_phase_code;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'form3_phase не найден (rf_project_report_id=%, phase=%)',
|
||||
p_rf_project_report_id, p_phase_code;
|
||||
END IF;
|
||||
|
||||
SELECT p.org_unit_id, ou.title, rpry.year, ou.utc_offset, rpr.report_type, rpr.project_id
|
||||
INTO v_org_unit_id, v_org_unit_title, v_year, v_utc_offset, v_report_type, v_project_id
|
||||
FROM v3.rf_project_report rpr
|
||||
JOIN v3.rf_project_year rpry ON rpr.rf_project_year_id = rpry.id
|
||||
LEFT JOIN v3.project p ON p.id = rpr.project_id
|
||||
LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id
|
||||
WHERE rpr.id = p_rf_project_report_id;
|
||||
v_task_name := trim(concat(
|
||||
'FORM_3',
|
||||
CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
|
||||
CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
|
||||
));
|
||||
|
||||
IF p_opens_at is not null THEN
|
||||
v_opens_at = (p_opens_at::text || v_utc_offset)::timestamptz;
|
||||
END IF;
|
||||
IF p_closes_at is not null THEN
|
||||
v_closes_at = (p_closes_at::text || v_utc_offset)::timestamptz;
|
||||
END IF;
|
||||
|
||||
UPDATE v3.form3_phase
|
||||
SET role = COALESCE(p_role, role),
|
||||
column_keys = COALESCE(p_column_keys, column_keys),
|
||||
opens_at = COALESCE(v_opens_at, opens_at),
|
||||
closes_at = COALESCE(v_closes_at, closes_at)
|
||||
WHERE rf_project_report_id = p_rf_project_report_id
|
||||
AND phase_code = p_phase_code
|
||||
RETURNING * INTO v_new;
|
||||
|
||||
|
||||
v_phase_id := format('%s:%s', p_rf_project_report_id, p_phase_code);
|
||||
|
||||
v_is_extend := v_new.closes_at > v_old.closes_at;
|
||||
v_has_non_extend_change := v_old.role IS DISTINCT FROM v_new.role
|
||||
OR v_old.column_keys IS DISTINCT FROM v_new.column_keys
|
||||
OR v_old.opens_at IS DISTINCT FROM v_new.opens_at;
|
||||
v_has_window_change := v_has_non_extend_change
|
||||
OR (v_old.closes_at IS DISTINCT FROM v_new.closes_at AND NOT v_is_extend);
|
||||
|
||||
IF v_old.role IS DISTINCT FROM v_new.role THEN
|
||||
v_changes := v_changes || jsonb_build_object(
|
||||
'role', jsonb_build_object('before', v_old.role, 'after', v_new.role)
|
||||
);
|
||||
END IF;
|
||||
IF v_old.column_keys IS DISTINCT FROM v_new.column_keys THEN
|
||||
v_changes := v_changes || jsonb_build_object(
|
||||
'column_keys', jsonb_build_object('before', v_old.column_keys, 'after', v_new.column_keys)
|
||||
);
|
||||
END IF;
|
||||
IF v_old.opens_at IS DISTINCT FROM v_new.opens_at THEN
|
||||
v_changes := v_changes || jsonb_build_object(
|
||||
'opens_at', jsonb_build_object(
|
||||
'before', v_old.opens_at,
|
||||
'after', v_new.opens_at
|
||||
)
|
||||
);
|
||||
END IF;
|
||||
IF v_old.closes_at IS DISTINCT FROM v_new.closes_at THEN
|
||||
v_changes := v_changes || jsonb_build_object(
|
||||
'closes_at', jsonb_build_object(
|
||||
'before', v_old.closes_at,
|
||||
'after', v_new.closes_at
|
||||
)
|
||||
);
|
||||
END IF;
|
||||
|
||||
IF v_is_extend THEN
|
||||
PERFORM v3.log_event(
|
||||
'ACCESS_EXTEND', 'ACCESS',
|
||||
jsonb_build_object(
|
||||
'entity_type', 'project',
|
||||
'entity_id', v_project_id,
|
||||
'core_entity_type', 'form3_phase',
|
||||
'core_entity_id', v_phase_id,
|
||||
'action', 'update',
|
||||
'phase_id', v_phase_id,
|
||||
'phase_name', p_phase_code,
|
||||
'task_name', NULLIF(v_task_name, ''),
|
||||
'report_id', p_rf_project_report_id,
|
||||
'report_type', v_report_type,
|
||||
'project_id', v_project_id,
|
||||
'org_unit_id', v_org_unit_id,
|
||||
'org_unit_name', v_org_unit_title,
|
||||
'year', v_year,
|
||||
'phase_code', p_phase_code,
|
||||
'closes_at_before', v_old.closes_at,
|
||||
'closes_at_after', v_new.closes_at
|
||||
),
|
||||
null, null, v_org_unit_id
|
||||
);
|
||||
END IF;
|
||||
|
||||
IF v_has_window_change THEN
|
||||
PERFORM v3.log_event(
|
||||
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
||||
jsonb_build_object(
|
||||
'entity_type', 'project',
|
||||
'entity_id', v_project_id,
|
||||
'core_entity_type', 'form3_phase',
|
||||
'core_entity_id', v_phase_id,
|
||||
'action', 'update',
|
||||
'phase_id', v_phase_id,
|
||||
'phase_name', p_phase_code,
|
||||
'task_id', p_rf_project_report_id,
|
||||
'task_name', NULLIF(v_task_name, ''),
|
||||
'project_id', v_project_id,
|
||||
'report_type', v_report_type,
|
||||
'year', v_year,
|
||||
'org_unit_id', v_org_unit_id,
|
||||
'org_unit_name', v_org_unit_title,
|
||||
'phase_code', p_phase_code,
|
||||
'changes', v_changes
|
||||
),
|
||||
null, null, v_org_unit_id
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN v_new;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
-- DROP FUNCTION v3.v_form3_project_jsonb(int4, varchar, _text);
|
||||
|
||||
CREATE OR REPLACE FUNCTION v3.v_form3_project_jsonb(p_project_id integer, p_report_type character varying, p_sections text[] DEFAULT NULL::text[])
|
||||
RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
AS $function$
|
||||
WITH reports AS (
|
||||
SELECT r.id AS report_id, rpry.year
|
||||
FROM v3.rf_project_report r
|
||||
JOIN v3.rf_project_year rpry ON rpry.id = r.rf_project_year_id
|
||||
WHERE r.project_id = p_project_id
|
||||
AND r.report_type = p_report_type
|
||||
),
|
||||
-- Разворачиваем каждый год в строки его листа
|
||||
per_year AS (
|
||||
SELECT
|
||||
v.row_type,
|
||||
v.depth,
|
||||
(v.data->'header'->>'expense_item_id')::INT AS eiid,
|
||||
v.data->'header' AS header,
|
||||
v.sort_order AS y_sort,
|
||||
rp.year AS year,
|
||||
v.data AS ydata
|
||||
FROM reports rp
|
||||
CROSS JOIN LATERAL v3.v_form3_report_jsonb(rp.report_id, p_sections) v
|
||||
),
|
||||
-- Сшиваем по идентичности строки (row_type + статья): сетка одинакова во всех годах
|
||||
agg AS (
|
||||
SELECT
|
||||
py.row_type,
|
||||
py.depth,
|
||||
py.eiid,
|
||||
MIN(py.y_sort) AS sort_order,
|
||||
(array_agg(py.header ORDER BY py.year))[1] AS header,
|
||||
jsonb_object_agg(py.year::text, py.ydata ORDER BY py.year) AS years,
|
||||
SUM((py.ydata->'totals'->>'total_actual')::numeric) AS project_total
|
||||
FROM per_year py
|
||||
GROUP BY py.row_type, py.depth, py.eiid
|
||||
)
|
||||
SELECT
|
||||
a.row_type,
|
||||
a.depth,
|
||||
ROW_NUMBER() OVER (ORDER BY a.sort_order) AS sort_order,
|
||||
jsonb_build_object(
|
||||
'header', a.header,
|
||||
'years', a.years,
|
||||
'project_total', a.project_total
|
||||
) AS data
|
||||
FROM agg a
|
||||
ORDER BY a.sort_order;
|
||||
$function$
|
||||
;
|
||||
-- DROP FUNCTION v3.v_form3_rf_rollup_sections(int4, int4, _text);
|
||||
|
||||
CREATE OR REPLACE FUNCTION v3.v_form3_rf_rollup_sections(p_branch_id integer, p_year integer, p_sections text[] DEFAULT NULL::text[])
|
||||
RETURNS TABLE(row_type character varying, depth integer, col_section_code character varying, col_item_id character varying, col_num_group_id character varying, col_name character varying, col_q1_adj_by_items numeric, col_q1_adj_increase numeric, col_q1_total_corr numeric, col_q1_m1 numeric, col_q1_m2 numeric, col_q1_m3 numeric, col_q1_quarter_actual numeric, col_q1_economy numeric, col_q2_adj_by_items numeric, col_q2_adj_increase numeric, col_q2_total_corr numeric, col_q2_m1 numeric, col_q2_m2 numeric, col_q2_m3 numeric, col_q2_quarter_actual numeric, col_q2_economy numeric, col_q3_adj_by_items numeric, col_q3_adj_increase numeric, col_q3_total_corr numeric, col_q3_m1 numeric, col_q3_m2 numeric, col_q3_m3 numeric, col_q3_quarter_actual numeric, col_q3_economy numeric, col_q4_adj_by_items numeric, col_q4_adj_increase numeric, col_q4_total_corr numeric, col_q4_m1 numeric, col_q4_m2 numeric, col_q4_m3 numeric, col_q4_spod numeric, col_q4_quarter_actual numeric, col_q4_economy numeric, col_year_total_corr numeric, col_year_total_actual numeric, col_year_economy numeric, _sort_path integer[])
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
AS $function$
|
||||
DECLARE
|
||||
s_q1 BOOLEAN := p_sections IS NULL OR 'q1' = ANY(p_sections);
|
||||
s_q2 BOOLEAN := p_sections IS NULL OR 'q2' = ANY(p_sections);
|
||||
s_q3 BOOLEAN := p_sections IS NULL OR 'q3' = ANY(p_sections);
|
||||
s_q4 BOOLEAN := p_sections IS NULL OR 'q4' = ANY(p_sections);
|
||||
s_year BOOLEAN := p_sections IS NULL OR 'year' = ANY(p_sections);
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
WITH
|
||||
tw AS (
|
||||
SELECT t.* FROM v3.mv_expense_item_tree t
|
||||
JOIN v3.expense_item_form_type eft
|
||||
ON eft.expense_item_id = t.id AND eft.form_type_code = 'FORM_3'
|
||||
),
|
||||
-- Агрегаты per (expense_item_id, quarter) — sum по обоим трекам всех проектов РФ × год
|
||||
agg_q AS (
|
||||
SELECT l.expense_item_id AS eid, q.quarter,
|
||||
SUM(q.adj_by_items) AS abi, SUM(q.adj_increase) AS ai,
|
||||
SUM(q.actual_m1) AS m1, SUM(q.actual_m2) AS m2, SUM(q.actual_m3) AS m3,
|
||||
SUM(q.actual_spod) AS spod
|
||||
FROM v3.rf_project_report r
|
||||
JOIN v3.rf_project_year rpry ON rpry.id = r.rf_project_year_id
|
||||
JOIN v3.project p ON p.id = r.project_id
|
||||
JOIN v3.rf_project_report_line l ON l.rf_project_report_id = r.id
|
||||
JOIN v3.rf_project_report_quarter q ON q.rf_project_report_line_id = l.id
|
||||
WHERE p.org_unit_id = p_branch_id AND rpry.year = p_year
|
||||
GROUP BY l.expense_item_id, q.quarter
|
||||
),
|
||||
tw_aq1 AS (SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
|
||||
SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3
|
||||
FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter=1
|
||||
GROUP BY tw.id),
|
||||
tw_aq2 AS (SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
|
||||
SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3
|
||||
FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter=2
|
||||
GROUP BY tw.id),
|
||||
tw_aq3 AS (SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
|
||||
SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3
|
||||
FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter=3
|
||||
GROUP BY tw.id),
|
||||
tw_aq4 AS (SELECT tw.id, SUM(a.abi) AS abi, SUM(a.ai) AS ai,
|
||||
SUM(a.m1) AS m1, SUM(a.m2) AS m2, SUM(a.m3) AS m3,
|
||||
SUM(a.spod) AS spod
|
||||
FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter=4
|
||||
GROUP BY tw.id)
|
||||
|
||||
SELECT
|
||||
CASE t.depth WHEN 0 THEN 'ROOT' WHEN 1 THEN 'GROUP'
|
||||
WHEN 2 THEN 'ITEM' WHEN 3 THEN 'SUB_ITEM' END::VARCHAR,
|
||||
t.depth,
|
||||
CASE WHEN t.depth < 3 THEN t.item_id ELSE t.parent_item_id END,
|
||||
CASE WHEN t.depth = 3 THEN t.item_id ELSE NULL END,
|
||||
t.num_group_id, t.name,
|
||||
-- Q1
|
||||
CASE WHEN s_q1 THEN a1.abi END,
|
||||
CASE WHEN s_q1 THEN a1.ai END,
|
||||
CASE WHEN s_q1 THEN COALESCE(a1.abi,0)+COALESCE(a1.ai,0) END,
|
||||
CASE WHEN s_q1 THEN a1.m1 END,
|
||||
CASE WHEN s_q1 THEN a1.m2 END,
|
||||
CASE WHEN s_q1 THEN a1.m3 END,
|
||||
CASE WHEN s_q1 THEN COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0) END,
|
||||
CASE WHEN s_q1 THEN ROUND((COALESCE(a1.abi,0)+COALESCE(a1.ai,0))
|
||||
-(COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)), 1) END,
|
||||
-- Q2
|
||||
CASE WHEN s_q2 THEN a2.abi END,
|
||||
CASE WHEN s_q2 THEN a2.ai END,
|
||||
CASE WHEN s_q2 THEN COALESCE(a2.abi,0)+COALESCE(a2.ai,0) END,
|
||||
CASE WHEN s_q2 THEN a2.m1 END,
|
||||
CASE WHEN s_q2 THEN a2.m2 END,
|
||||
CASE WHEN s_q2 THEN a2.m3 END,
|
||||
CASE WHEN s_q2 THEN COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0) END,
|
||||
CASE WHEN s_q2 THEN ROUND((COALESCE(a2.abi,0)+COALESCE(a2.ai,0))
|
||||
-(COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)), 1) END,
|
||||
-- Q3
|
||||
CASE WHEN s_q3 THEN a3.abi END,
|
||||
CASE WHEN s_q3 THEN a3.ai END,
|
||||
CASE WHEN s_q3 THEN COALESCE(a3.abi,0)+COALESCE(a3.ai,0) END,
|
||||
CASE WHEN s_q3 THEN a3.m1 END,
|
||||
CASE WHEN s_q3 THEN a3.m2 END,
|
||||
CASE WHEN s_q3 THEN a3.m3 END,
|
||||
CASE WHEN s_q3 THEN COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0) END,
|
||||
CASE WHEN s_q3 THEN ROUND((COALESCE(a3.abi,0)+COALESCE(a3.ai,0))
|
||||
-(COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)), 1) END,
|
||||
-- Q4
|
||||
CASE WHEN s_q4 THEN a4.abi END,
|
||||
CASE WHEN s_q4 THEN a4.ai END,
|
||||
CASE WHEN s_q4 THEN COALESCE(a4.abi,0)+COALESCE(a4.ai,0) END,
|
||||
CASE WHEN s_q4 THEN a4.m1 END,
|
||||
CASE WHEN s_q4 THEN a4.m2 END,
|
||||
CASE WHEN s_q4 THEN a4.m3 END,
|
||||
CASE WHEN s_q4 THEN a4.spod END,
|
||||
CASE WHEN s_q4 THEN COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0) END,
|
||||
CASE WHEN s_q4 THEN ROUND((COALESCE(a4.abi,0)+COALESCE(a4.ai,0))
|
||||
-(COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0)), 1) END,
|
||||
-- year
|
||||
CASE WHEN s_year THEN COALESCE(a1.abi,0)+COALESCE(a1.ai,0)+COALESCE(a2.abi,0)+COALESCE(a2.ai,0)+
|
||||
COALESCE(a3.abi,0)+COALESCE(a3.ai,0)+COALESCE(a4.abi,0)+COALESCE(a4.ai,0) END,
|
||||
CASE WHEN s_year THEN COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)+
|
||||
COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)+
|
||||
COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)+
|
||||
COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0) END,
|
||||
CASE WHEN s_year THEN
|
||||
ROUND(
|
||||
(COALESCE(a1.abi,0)+COALESCE(a1.ai,0)+COALESCE(a2.abi,0)+COALESCE(a2.ai,0)+
|
||||
COALESCE(a3.abi,0)+COALESCE(a3.ai,0)+COALESCE(a4.abi,0)+COALESCE(a4.ai,0))
|
||||
-(COALESCE(a1.m1,0)+COALESCE(a1.m2,0)+COALESCE(a1.m3,0)+
|
||||
COALESCE(a2.m1,0)+COALESCE(a2.m2,0)+COALESCE(a2.m3,0)+
|
||||
COALESCE(a3.m1,0)+COALESCE(a3.m2,0)+COALESCE(a3.m3,0)+
|
||||
COALESCE(a4.m1,0)+COALESCE(a4.m2,0)+COALESCE(a4.m3,0)+COALESCE(a4.spod,0)),
|
||||
1)
|
||||
END,
|
||||
t.path
|
||||
FROM tw t
|
||||
LEFT JOIN tw_aq1 a1 ON a1.id = t.id
|
||||
LEFT JOIN tw_aq2 a2 ON a2.id = t.id
|
||||
LEFT JOIN tw_aq3 a3 ON a3.id = t.id
|
||||
LEFT JOIN tw_aq4 a4 ON a4.id = t.id
|
||||
ORDER BY t.path;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION v3.copy_template_to_report(p_report_id integer)
|
||||
RETURNS integer
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_report_type VARCHAR;
|
||||
v_inserted INTEGER;
|
||||
v_org_unit INTEGER;
|
||||
v_utc_offset VARCHAR(3);
|
||||
v_year INTEGER;
|
||||
BEGIN
|
||||
SELECT report_type, org_unit_id, "year" INTO v_report_type, v_org_unit, v_year
|
||||
FROM v3.rf_project_report AS rpr
|
||||
INNER JOIN v3.project AS p ON p.id = rpr.project_id
|
||||
INNER JOIN v3.rf_project_year AS rpy ON rpy.id = rpr.rf_project_year_id
|
||||
WHERE rpr.id = p_report_id;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
|
||||
END IF;
|
||||
|
||||
SELECT utc_offset INTO v_utc_offset
|
||||
FROM v3.org_unit
|
||||
WHERE id = v_org_unit;
|
||||
|
||||
INSERT INTO v3.form3_phase
|
||||
(rf_project_report_id, phase_code, role, column_keys, opens_at, closes_at)
|
||||
SELECT p_report_id, pt.phase_code, pt.role,
|
||||
pt.column_keys,
|
||||
((pt.opens_at + INTERVAL '1 year' * v_year)::text || v_utc_offset)::timestamptz,
|
||||
((pt.closes_at + INTERVAL '1 year' * v_year)::text || v_utc_offset)::timestamptz
|
||||
FROM v3.phase_template pt
|
||||
WHERE pt.form_type = 'FORM_3'
|
||||
AND pt.sheet = v_report_type
|
||||
ON CONFLICT (rf_project_report_id, phase_code) DO NOTHING;
|
||||
|
||||
GET DIAGNOSTICS v_inserted = ROW_COUNT;
|
||||
RETURN v_inserted;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
-- DROP FUNCTION v3.del_project(int4);
|
||||
|
||||
CREATE OR REPLACE FUNCTION v3.del_project(p_project_id integer)
|
||||
RETURNS integer
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_old RECORD;
|
||||
v_actor_id INTEGER;
|
||||
v_role VARCHAR;
|
||||
BEGIN
|
||||
SELECT p.name, p.level, p.org_unit_id
|
||||
INTO v_old
|
||||
FROM v3.project p WHERE p.id = p_project_id;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'project #% не существует', p_project_id;
|
||||
END IF;
|
||||
|
||||
IF NOT v3.can_delete_project(p_project_id) THEN
|
||||
RAISE EXCEPTION 'project #% невозможно удалить', p_project_id;
|
||||
END IF;
|
||||
|
||||
DELETE FROM v3.form3_phase f3p WHERE f3p.rf_project_report_id IN (
|
||||
SELECT id FROM v3.rf_project_report rpr WHERE project_id = p_project_id
|
||||
);
|
||||
DELETE FROM v3.rf_project_report_quarter
|
||||
WHERE rf_project_report_line_id IN (
|
||||
SELECT l.id FROM v3.rf_project_report_line l
|
||||
JOIN v3.rf_project_report r ON r.id = l.rf_project_report_id
|
||||
WHERE r.project_id = p_project_id);
|
||||
DELETE FROM v3.rf_project_report_line
|
||||
WHERE rf_project_report_id IN (
|
||||
SELECT id FROM v3.rf_project_report WHERE project_id = p_project_id);
|
||||
DELETE FROM v3.rf_project_report WHERE project_id = p_project_id;
|
||||
DELETE FROM v3.smeta WHERE rf_project_year_id IN (
|
||||
SELECT id FROM v3.rf_project_year WHERE project_id = p_project_id
|
||||
);
|
||||
DELETE FROM v3.rf_project_year WHERE project_id = p_project_id;
|
||||
DELETE FROM v3.project WHERE id = p_project_id;
|
||||
|
||||
PERFORM v3.log_event(
|
||||
'PROJECT_DELETE', 'PROJECT',
|
||||
jsonb_build_object(
|
||||
'project_id', p_project_id,
|
||||
'name', v_old.name,
|
||||
'level', v_old.level,
|
||||
'org_unit_id', v_old.org_unit_id), null, null, v_old.org_unit_id);
|
||||
|
||||
RETURN p_project_id;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
|
||||
-- DROP FUNCTION v3.upd_project(int4, varchar, varchar, varchar, varchar, varchar, varchar, varchar, int4, numeric, int4, date, date, date, date, varchar);
|
||||
|
||||
CREATE OR REPLACE FUNCTION v3.upd_project(
|
||||
p_project_id integer,
|
||||
p_name character varying DEFAULT NULL,
|
||||
p_status character varying DEFAULT NULL,
|
||||
p_technical_number character varying DEFAULT NULL,
|
||||
p_project_type character varying DEFAULT NULL,
|
||||
p_vsp_format character varying DEFAULT NULL,
|
||||
p_placement_type character varying DEFAULT NULL,
|
||||
p_object_address character varying DEFAULT NULL,
|
||||
p_staff_count integer DEFAULT NULL,
|
||||
p_total_area numeric DEFAULT NULL,
|
||||
p_org_unit_id integer DEFAULT NULL,
|
||||
p_krf_decision_date date DEFAULT NULL,
|
||||
p_fk_decision_date date DEFAULT NULL,
|
||||
p_board_decision_date date DEFAULT NULL,
|
||||
p_open_relocate_close_date date DEFAULT NULL,
|
||||
p_funding_by_ko_decision character varying DEFAULT NULL
|
||||
)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_old v3.project%ROWTYPE;
|
||||
v_changes JSONB := '{}'::jsonb;
|
||||
BEGIN
|
||||
SELECT * INTO v_old FROM v3.project WHERE id = p_project_id;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'project id=% не существует', p_project_id;
|
||||
END IF;
|
||||
|
||||
IF p_name IS NOT NULL AND p_name IS DISTINCT FROM v_old.name THEN
|
||||
UPDATE v3.project SET name = p_name WHERE id = p_project_id;
|
||||
v_changes := v_changes || jsonb_build_object('name',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.name), 'after', v3._diff_val(p_name)));
|
||||
END IF;
|
||||
|
||||
IF p_status IS NOT NULL AND p_status IS DISTINCT FROM v_old.status THEN
|
||||
UPDATE v3.project SET status = p_status WHERE id = p_project_id;
|
||||
v_changes := v_changes || jsonb_build_object('status',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.status), 'after', v3._diff_val(p_status)));
|
||||
END IF;
|
||||
|
||||
IF p_technical_number IS NOT NULL AND p_technical_number IS DISTINCT FROM v_old.technical_number THEN
|
||||
UPDATE v3.project SET technical_number = p_technical_number WHERE id = p_project_id;
|
||||
v_changes := v_changes || jsonb_build_object('technical_number',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.technical_number), 'after', v3._diff_val(p_technical_number)));
|
||||
END IF;
|
||||
|
||||
IF p_project_type IS NOT NULL AND p_project_type IS DISTINCT FROM v_old.project_type THEN
|
||||
UPDATE v3.project SET project_type = p_project_type WHERE id = p_project_id;
|
||||
v_changes := v_changes || jsonb_build_object('project_type',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.project_type), 'after', v3._diff_val(p_project_type)));
|
||||
END IF;
|
||||
|
||||
IF p_vsp_format IS NOT NULL AND p_vsp_format IS DISTINCT FROM v_old.vsp_format THEN
|
||||
UPDATE v3.project SET vsp_format = p_vsp_format WHERE id = p_project_id;
|
||||
v_changes := v_changes || jsonb_build_object('vsp_format',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.vsp_format), 'after', v3._diff_val(p_vsp_format)));
|
||||
END IF;
|
||||
|
||||
IF p_placement_type IS NOT NULL AND p_placement_type IS DISTINCT FROM v_old.placement_type THEN
|
||||
UPDATE v3.project SET placement_type = p_placement_type WHERE id = p_project_id;
|
||||
v_changes := v_changes || jsonb_build_object('placement_type',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.placement_type), 'after', v3._diff_val(p_placement_type)));
|
||||
END IF;
|
||||
|
||||
IF p_object_address IS NOT NULL AND p_object_address IS DISTINCT FROM v_old.object_address THEN
|
||||
UPDATE v3.project SET object_address = p_object_address WHERE id = p_project_id;
|
||||
v_changes := v_changes || jsonb_build_object('object_address',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.object_address), 'after', v3._diff_val(p_object_address)));
|
||||
END IF;
|
||||
|
||||
IF p_staff_count IS NOT NULL AND p_staff_count IS DISTINCT FROM v_old.staff_count THEN
|
||||
UPDATE v3.project SET staff_count = p_staff_count WHERE id = p_project_id;
|
||||
v_changes := v_changes || jsonb_build_object('staff_count',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.staff_count), 'after', v3._diff_val(p_staff_count)));
|
||||
END IF;
|
||||
|
||||
IF p_total_area IS NOT NULL AND p_total_area IS DISTINCT FROM v_old.total_area THEN
|
||||
UPDATE v3.project SET total_area = p_total_area WHERE id = p_project_id;
|
||||
v_changes := v_changes || jsonb_build_object('total_area',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.total_area), 'after', v3._diff_val(p_total_area)));
|
||||
END IF;
|
||||
|
||||
IF p_org_unit_id IS NOT NULL AND p_org_unit_id IS DISTINCT FROM v_old.org_unit_id THEN
|
||||
IF NOT EXISTS (SELECT 1 FROM v3.org_unit WHERE id = p_org_unit_id) THEN
|
||||
RAISE EXCEPTION 'org_unit id=% не существует', p_org_unit_id;
|
||||
END IF;
|
||||
UPDATE v3.project SET org_unit_id = p_org_unit_id WHERE id = p_project_id;
|
||||
v_changes := v_changes || jsonb_build_object('org_unit_id',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.org_unit_id), 'after', v3._diff_val(p_org_unit_id)));
|
||||
END IF;
|
||||
|
||||
IF p_krf_decision_date IS NOT NULL AND p_krf_decision_date IS DISTINCT FROM v_old.krf_decision_date THEN
|
||||
UPDATE v3.project SET krf_decision_date = p_krf_decision_date WHERE id = p_project_id;
|
||||
v_changes := v_changes || jsonb_build_object('krf_decision_date',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.krf_decision_date), 'after', v3._diff_val(p_krf_decision_date)));
|
||||
END IF;
|
||||
|
||||
IF p_fk_decision_date IS NOT NULL AND p_fk_decision_date IS DISTINCT FROM v_old.fk_decision_date THEN
|
||||
UPDATE v3.project SET fk_decision_date = p_fk_decision_date WHERE id = p_project_id;
|
||||
v_changes := v_changes || jsonb_build_object('fk_decision_date',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.fk_decision_date), 'after', v3._diff_val(p_fk_decision_date)));
|
||||
END IF;
|
||||
|
||||
IF p_board_decision_date IS NOT NULL AND p_board_decision_date IS DISTINCT FROM v_old.board_decision_date THEN
|
||||
UPDATE v3.project SET board_decision_date = p_board_decision_date WHERE id = p_project_id;
|
||||
v_changes := v_changes || jsonb_build_object('board_decision_date',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.board_decision_date), 'after', v3._diff_val(p_board_decision_date)));
|
||||
END IF;
|
||||
|
||||
IF p_open_relocate_close_date IS NOT NULL AND p_open_relocate_close_date IS DISTINCT FROM v_old.open_relocate_close_date THEN
|
||||
UPDATE v3.project SET open_relocate_close_date = p_open_relocate_close_date WHERE id = p_project_id;
|
||||
v_changes := v_changes || jsonb_build_object('open_relocate_close_date',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.open_relocate_close_date), 'after', v3._diff_val(p_open_relocate_close_date)));
|
||||
END IF;
|
||||
|
||||
IF p_funding_by_ko_decision IS NOT NULL AND p_funding_by_ko_decision IS DISTINCT FROM v_old.funding_by_ko_decision THEN
|
||||
UPDATE v3.project SET funding_by_ko_decision = p_funding_by_ko_decision WHERE id = p_project_id;
|
||||
v_changes := v_changes || jsonb_build_object('funding_by_ko_decision',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.funding_by_ko_decision), 'after', v3._diff_val(p_funding_by_ko_decision)));
|
||||
END IF;
|
||||
|
||||
IF v_changes <> '{}'::jsonb THEN
|
||||
PERFORM v3.log_event(
|
||||
'PROJECT_UPDATE', 'PROJECT',
|
||||
jsonb_build_object(
|
||||
'project_id', p_project_id,
|
||||
'entity_id', p_project_id,
|
||||
'changes', v_changes
|
||||
), null, null, COALESCE(p_org_unit_id, v_old.org_unit_id)
|
||||
);
|
||||
END IF;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
|
||||
-- DROP FUNCTION v3.upd_smeta(int4, bool, bool, bool, bool, varchar, numeric, numeric);
|
||||
|
||||
CREATE OR REPLACE FUNCTION v3.upd_smeta(
|
||||
p_rf_project_year_id integer,
|
||||
p_is_in_plan boolean DEFAULT NULL,
|
||||
p_is_in_plan_q2 boolean DEFAULT NULL,
|
||||
p_is_in_plan_q3 boolean DEFAULT NULL,
|
||||
p_is_in_plan_q4 boolean DEFAULT NULL,
|
||||
p_development_block character varying DEFAULT NULL,
|
||||
p_reserve_to_prrs_ahr numeric DEFAULT NULL,
|
||||
p_reserve_to_prrs_kv numeric DEFAULT NULL
|
||||
)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_old v3.smeta%ROWTYPE;
|
||||
v_changes JSONB := '{}'::jsonb;
|
||||
v_project_id INT;
|
||||
v_org_unit_id INT;
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM v3.rf_project_year WHERE id = p_rf_project_year_id) THEN
|
||||
RAISE EXCEPTION 'rf_project_year id=% не существует', p_rf_project_year_id;
|
||||
END IF;
|
||||
|
||||
INSERT INTO v3.smeta (rf_project_year_id)
|
||||
SELECT p_rf_project_year_id
|
||||
WHERE NOT EXISTS (SELECT 1 FROM v3.smeta WHERE rf_project_year_id = p_rf_project_year_id);
|
||||
|
||||
SELECT * INTO v_old FROM v3.smeta WHERE rf_project_year_id = p_rf_project_year_id;
|
||||
|
||||
IF p_is_in_plan IS NOT NULL AND p_is_in_plan IS DISTINCT FROM v_old.is_in_plan THEN
|
||||
UPDATE v3.smeta SET is_in_plan = p_is_in_plan WHERE id = v_old.id;
|
||||
v_changes := v_changes || jsonb_build_object('is_in_plan',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.is_in_plan), 'after', v3._diff_val(p_is_in_plan)));
|
||||
END IF;
|
||||
|
||||
IF p_is_in_plan_q2 IS NOT NULL AND p_is_in_plan_q2 IS DISTINCT FROM v_old.is_in_plan_q2 THEN
|
||||
UPDATE v3.smeta SET is_in_plan_q2 = p_is_in_plan_q2 WHERE id = v_old.id;
|
||||
v_changes := v_changes || jsonb_build_object('is_in_plan_q2',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.is_in_plan_q2), 'after', v3._diff_val(p_is_in_plan_q2)));
|
||||
END IF;
|
||||
|
||||
IF p_is_in_plan_q3 IS NOT NULL AND p_is_in_plan_q3 IS DISTINCT FROM v_old.is_in_plan_q3 THEN
|
||||
UPDATE v3.smeta SET is_in_plan_q3 = p_is_in_plan_q3 WHERE id = v_old.id;
|
||||
v_changes := v_changes || jsonb_build_object('is_in_plan_q3',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.is_in_plan_q3), 'after', v3._diff_val(p_is_in_plan_q3)));
|
||||
END IF;
|
||||
|
||||
IF p_is_in_plan_q4 IS NOT NULL AND p_is_in_plan_q4 IS DISTINCT FROM v_old.is_in_plan_q4 THEN
|
||||
UPDATE v3.smeta SET is_in_plan_q4 = p_is_in_plan_q4 WHERE id = v_old.id;
|
||||
v_changes := v_changes || jsonb_build_object('is_in_plan_q4',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.is_in_plan_q4), 'after', v3._diff_val(p_is_in_plan_q4)));
|
||||
END IF;
|
||||
|
||||
IF p_development_block IS NOT NULL AND p_development_block IS DISTINCT FROM v_old.development_block THEN
|
||||
UPDATE v3.smeta SET development_block = p_development_block WHERE id = v_old.id;
|
||||
v_changes := v_changes || jsonb_build_object('development_block',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.development_block), 'after', v3._diff_val(p_development_block)));
|
||||
END IF;
|
||||
|
||||
IF p_reserve_to_prrs_ahr IS NOT NULL AND p_reserve_to_prrs_ahr IS DISTINCT FROM v_old.reserve_to_prrs_ahr THEN
|
||||
UPDATE v3.smeta SET reserve_to_prrs_ahr = p_reserve_to_prrs_ahr WHERE id = v_old.id;
|
||||
v_changes := v_changes || jsonb_build_object('reserve_to_prrs_ahr',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.reserve_to_prrs_ahr), 'after', v3._diff_val(p_reserve_to_prrs_ahr)));
|
||||
END IF;
|
||||
|
||||
IF p_reserve_to_prrs_kv IS NOT NULL AND p_reserve_to_prrs_kv IS DISTINCT FROM v_old.reserve_to_prrs_kv THEN
|
||||
UPDATE v3.smeta SET reserve_to_prrs_kv = p_reserve_to_prrs_kv WHERE id = v_old.id;
|
||||
v_changes := v_changes || jsonb_build_object('reserve_to_prrs_kv',
|
||||
jsonb_build_object('before', v3._diff_val(v_old.reserve_to_prrs_kv), 'after', v3._diff_val(p_reserve_to_prrs_kv)));
|
||||
END IF;
|
||||
|
||||
SELECT rpy.project_id, p.org_unit_id
|
||||
INTO v_project_id, v_org_unit_id
|
||||
FROM v3.rf_project_year rpy
|
||||
JOIN v3.project p ON p.id = rpy.project_id
|
||||
WHERE rpy.id = p_rf_project_year_id;
|
||||
|
||||
IF v_changes <> '{}'::jsonb THEN
|
||||
PERFORM v3.log_event(
|
||||
'SMETA_UPDATE', 'PROJECT',
|
||||
jsonb_build_object(
|
||||
'rf_project_year_id', p_rf_project_year_id,
|
||||
'project_id', v_project_id,
|
||||
'changes', v_changes
|
||||
), null, null, v_org_unit_id
|
||||
);
|
||||
END IF;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
@ -198,6 +198,7 @@ async def get_form3_phases(
|
||||
project_id=project_id,
|
||||
year=year,
|
||||
report_type=report_type,
|
||||
load_year=True,
|
||||
)
|
||||
|
||||
phases = await f3p_service.get_list(
|
||||
|
||||
@ -18,6 +18,7 @@ from src.domain.schemas import (
|
||||
ResponseBase,
|
||||
SheetResponse,
|
||||
UpdProjectBody,
|
||||
UpdSmetaBody,
|
||||
)
|
||||
from src.services.project_service import ProjectService
|
||||
|
||||
@ -67,6 +68,27 @@ async def get_projects(
|
||||
return BaseListResponse(result=projects, count=count)
|
||||
|
||||
|
||||
@router.get("/projects/with-reports")
|
||||
async def get_projects_with_reports(
|
||||
offset: int = 0,
|
||||
limit: int = 100,
|
||||
year: Optional[int] = None,
|
||||
branch_id: Optional[int] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||
) -> BaseListResponse[dict]:
|
||||
project_service = ProjectService(db)
|
||||
count, projects = await project_service.get_list_with_reports(
|
||||
user=current_user,
|
||||
with_count=True,
|
||||
year=year,
|
||||
branch_id=branch_id,
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
)
|
||||
return BaseListResponse(result=projects, count=count)
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}")
|
||||
async def get_project(
|
||||
project_id: int,
|
||||
@ -243,13 +265,28 @@ async def upd_project(
|
||||
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||
) -> dict:
|
||||
project_service = ProjectService(db)
|
||||
result = await project_service.upd_project(
|
||||
return await project_service.upd_project(
|
||||
project_id=project_id,
|
||||
column=body.column,
|
||||
value=body.value,
|
||||
data=body.model_dump(exclude_unset=True),
|
||||
user=current_user,
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/projects/{project_id}/smeta/{year}")
|
||||
async def upd_smeta(
|
||||
project_id: int,
|
||||
year: int,
|
||||
body: UpdSmetaBody,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||
) -> dict:
|
||||
project_service = ProjectService(db)
|
||||
return await project_service.upd_smeta(
|
||||
project_id=project_id,
|
||||
year=year,
|
||||
data=body.model_dump(exclude_unset=True),
|
||||
user=current_user,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.delete("/projects/{project_id}")
|
||||
|
||||
@ -25,9 +25,11 @@ from src.db.models.reserve import Reserve
|
||||
from src.db.models.rf_project_report import RfProjectReport
|
||||
from src.db.models.rf_project_report_line import RfProjectReportLine
|
||||
from src.db.models.rf_project_report_quarter import RfProjectReportQuarter
|
||||
from src.db.models.rf_project_year import RfProjectYear
|
||||
from src.db.models.role import Role, UserRoleEnum
|
||||
from src.db.models.security_detail import SecurityDetail
|
||||
from src.db.models.sequestration import Sequestration
|
||||
from src.db.models.smeta import Smeta
|
||||
from src.db.models.user_org import UserOrg
|
||||
from src.db.models.utility_detail import UtilityDetail
|
||||
from src.db.models.vsp import Vsp
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import date, datetime
|
||||
|
||||
from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Index, Integer, Numeric, String, func, text
|
||||
from sqlalchemy import CheckConstraint, Date, DateTime, ForeignKey, Index, Integer, Numeric, String, func, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src.db.base import Base
|
||||
@ -28,6 +28,14 @@ class Project(Base):
|
||||
text("name IS NOT NULL AND name <> ''"),
|
||||
name="chk_v3_project_name",
|
||||
),
|
||||
CheckConstraint(
|
||||
"status IS NULL OR status IN ('created', 'agreed', 'archived')",
|
||||
name="project_status_check",
|
||||
),
|
||||
CheckConstraint(
|
||||
"funding_by_ko_decision IS NULL OR funding_by_ko_decision IN ('prrs_budget', 'bank_reserve')",
|
||||
name="project_funding_ko_check",
|
||||
),
|
||||
Index("ix_v3_project_ssp", "org_unit_id"),
|
||||
{"schema": "v3"},
|
||||
)
|
||||
@ -44,6 +52,13 @@ class Project(Base):
|
||||
total_area: Mapped[float | None] = mapped_column(Numeric)
|
||||
org_unit_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("v3.org_unit.id"))
|
||||
created_at: Mapped[datetime | None] = mapped_column(DateTime, server_default=func.now())
|
||||
status: Mapped[str | None] = mapped_column(String, server_default=text("'created'"))
|
||||
technical_number: Mapped[str | None] = mapped_column(String)
|
||||
krf_decision_date: Mapped[date | None] = mapped_column(Date)
|
||||
fk_decision_date: Mapped[date | None] = mapped_column(Date)
|
||||
board_decision_date: Mapped[date | None] = mapped_column(Date)
|
||||
open_relocate_close_date: Mapped[date | None] = mapped_column(Date)
|
||||
funding_by_ko_decision: Mapped[str | None] = mapped_column(String)
|
||||
|
||||
# parent = relationship("Project", remote_side="Project.id", back_populates="children")
|
||||
# children: Mapped[list["Project"]] = relationship("Project", back_populates="parent") # type: ignore
|
||||
|
||||
@ -11,14 +11,14 @@ from src.db.base import Base
|
||||
class RfProjectReport(Base):
|
||||
__tablename__ = "rf_project_report"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("project_id", "year", "report_type", name="uq_v3_rf_project_report"),
|
||||
Index("ix_v3_rf_project_report_proj_year", "project_id", "year"),
|
||||
UniqueConstraint("project_id", "rf_project_year_id", "report_type", name="uq_v3_rf_project_report"),
|
||||
Index("ix_v3_rf_project_report_proj_year", "project_id", "rf_project_year_id"),
|
||||
{"schema": "v3"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
project_id: Mapped[int] = mapped_column(Integer, ForeignKey("v3.project.id"))
|
||||
year: Mapped[int] = mapped_column(Integer)
|
||||
rf_project_year_id: Mapped[int] = mapped_column(Integer, ForeignKey("v3.rf_project_year.id"))
|
||||
report_type: Mapped[str] = mapped_column(String, CheckConstraint("report_type IN ('LIMIT','CURRENT_EXPENSES')"))
|
||||
created_by: Mapped[int | None] = mapped_column(Integer, ForeignKey("v3.app_user.id"))
|
||||
created_at: Mapped[datetime | None] = mapped_column(DateTime, server_default=func.now())
|
||||
@ -26,6 +26,11 @@ class RfProjectReport(Base):
|
||||
updated_at: Mapped[datetime | None] = mapped_column(DateTime, server_default=func.now())
|
||||
|
||||
project: Mapped["Project"] = relationship("Project", back_populates="rf_project_reports") # type: ignore
|
||||
rf_project_year: Mapped["RfProjectYear"] = relationship("RfProjectYear") # type: ignore
|
||||
|
||||
@property
|
||||
def year(self):
|
||||
return self.rf_project_year.year
|
||||
# creator: Mapped[AppUser | None] = relationship("AppUser", foreign_keys=[created_by])
|
||||
# updater: Mapped[AppUser | None] = relationship("AppUser", foreign_keys=[updated_by])
|
||||
# lines: Mapped[list[RfProjectReportLine]] = relationship("RfProjectReportLine", back_populates="report")
|
||||
|
||||
22
api/src/db/models/rf_project_year.py
Normal file
22
api/src/db/models/rf_project_year.py
Normal file
@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import ForeignKey, Index, Integer, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src.db.base import Base
|
||||
|
||||
|
||||
class RfProjectYear(Base):
|
||||
__tablename__ = "rf_project_year"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("project_id", "year", name="uq_v3_rf_project_year"),
|
||||
Index("ix_v3_rf_project_year_project", "project_id"),
|
||||
{"schema": "v3"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
project_id: Mapped[int] = mapped_column(Integer, ForeignKey("v3.project.id"))
|
||||
year: Mapped[int] = mapped_column(Integer)
|
||||
|
||||
project: Mapped["Project"] = relationship("Project") # type: ignore
|
||||
smetas: Mapped[list["Smeta"]] = relationship("Smeta", back_populates="rf_project_year") # type: ignore
|
||||
29
api/src/db/models/smeta.py
Normal file
29
api/src/db/models/smeta.py
Normal file
@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import Boolean, CheckConstraint, ForeignKey, Integer, Numeric, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src.db.base import Base
|
||||
|
||||
|
||||
class Smeta(Base):
|
||||
__tablename__ = "smeta"
|
||||
__table_args__ = (
|
||||
CheckConstraint(
|
||||
"development_block IS NULL OR development_block IN ('out_of_base_plan', 'carryover', 'additional_funding')",
|
||||
name="smeta_development_block_check",
|
||||
),
|
||||
{"schema": "v3"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
rf_project_year_id: Mapped[int] = mapped_column(Integer, ForeignKey("v3.rf_project_year.id"))
|
||||
is_in_plan: Mapped[bool | None] = mapped_column(Boolean)
|
||||
is_in_plan_q2: Mapped[bool | None] = mapped_column(Boolean)
|
||||
is_in_plan_q3: Mapped[bool | None] = mapped_column(Boolean)
|
||||
is_in_plan_q4: Mapped[bool | None] = mapped_column(Boolean)
|
||||
development_block: Mapped[str | None] = mapped_column(String)
|
||||
reserve_to_prrs_ahr: Mapped[float | None] = mapped_column(Numeric)
|
||||
reserve_to_prrs_kv: Mapped[float | None] = mapped_column(Numeric)
|
||||
|
||||
rf_project_year: Mapped["RfProjectYear"] = relationship("RfProjectYear", back_populates="smetas") # type: ignore
|
||||
@ -37,6 +37,28 @@ PlacementTypeLiteral = Literal[
|
||||
"Субаренда",
|
||||
]
|
||||
|
||||
PLACEMENT_TYPE_TO_DB: dict[str, str] = {
|
||||
"Собственность": "own",
|
||||
"Аренда": "rent",
|
||||
"Субаренда": "other",
|
||||
}
|
||||
|
||||
PLACEMENT_TYPE_FROM_DB: dict[str, str] = {
|
||||
db_value: label for label, db_value in PLACEMENT_TYPE_TO_DB.items()
|
||||
}
|
||||
|
||||
|
||||
def placement_type_to_db(value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
return PLACEMENT_TYPE_TO_DB.get(value, value)
|
||||
|
||||
|
||||
def placement_type_from_db(value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
return PLACEMENT_TYPE_FROM_DB.get(value, value)
|
||||
|
||||
|
||||
# def serialize_missing_greenlet(cls, obj):
|
||||
# result = {}
|
||||
@ -356,8 +378,35 @@ class AddForm3LineBody(BaseModel):
|
||||
|
||||
|
||||
class UpdProjectBody(BaseModel):
|
||||
column: str
|
||||
value: Any = None
|
||||
name: Optional[str] = Field(
|
||||
None,
|
||||
max_length=30,
|
||||
pattern=r"^[^+\-\/\\=&*\s]{1,30}$",
|
||||
)
|
||||
status: Optional[Literal["created", "agreed", "archived"]] = None
|
||||
technical_number: Optional[str] = None
|
||||
project_type: Optional[ProjectTypeLiteral] = None
|
||||
vsp_format: Optional[VspFormatLiteral] = None
|
||||
placement_type: Optional[PlacementTypeLiteral] = None
|
||||
object_address: Optional[str] = None
|
||||
staff_count: Optional[int] = None
|
||||
total_area: Optional[float] = None
|
||||
org_unit_id: Optional[int] = None
|
||||
krf_decision_date: Optional[date] = None
|
||||
fk_decision_date: Optional[date] = None
|
||||
board_decision_date: Optional[date] = None
|
||||
open_relocate_close_date: Optional[date] = None
|
||||
funding_by_ko_decision: Optional[Literal["prrs_budget", "bank_reserve"]] = None
|
||||
|
||||
|
||||
class UpdSmetaBody(BaseModel):
|
||||
is_in_plan: Optional[bool] = None
|
||||
is_in_plan_q2: Optional[bool] = None
|
||||
is_in_plan_q3: Optional[bool] = None
|
||||
is_in_plan_q4: Optional[bool] = None
|
||||
development_block: Optional[Literal["out_of_base_plan", "carryover", "additional_funding"]] = None
|
||||
reserve_to_prrs_ahr: Optional[float] = None
|
||||
reserve_to_prrs_kv: Optional[float] = None
|
||||
|
||||
|
||||
class AddProjectBody(BaseModel):
|
||||
|
||||
@ -7,6 +7,9 @@ from src.db.models.org_unit import OrgUnit
|
||||
from src.db.models.project import Project
|
||||
from src.db.models.rf_project_report import RfProjectReport
|
||||
from src.db.models.rf_project_report_line import RfProjectReportLine
|
||||
from src.db.models.rf_project_year import RfProjectYear
|
||||
from src.db.models.smeta import Smeta
|
||||
from src.domain.schemas import placement_type_from_db
|
||||
|
||||
|
||||
class ProjectRepository:
|
||||
@ -22,7 +25,7 @@ class ProjectRepository:
|
||||
"parent_id": project.parent_id,
|
||||
"project_type": project.project_type,
|
||||
"vsp_format": project.vsp_format,
|
||||
"placement_type": project.placement_type,
|
||||
"placement_type": placement_type_from_db(project.placement_type),
|
||||
"object_address": project.object_address,
|
||||
"staff_count": project.staff_count,
|
||||
"total_area": float(project.total_area) if project.total_area is not None else None,
|
||||
@ -38,9 +41,12 @@ class ProjectRepository:
|
||||
if year is not None:
|
||||
query = query.where(
|
||||
exists(
|
||||
select(1).where(
|
||||
select(1)
|
||||
.select_from(RfProjectReport)
|
||||
.join(RfProjectYear, RfProjectYear.id == RfProjectReport.rf_project_year_id)
|
||||
.where(
|
||||
RfProjectReport.project_id == Project.id,
|
||||
RfProjectReport.year == year,
|
||||
RfProjectYear.year == year,
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -69,7 +75,9 @@ class ProjectRepository:
|
||||
)
|
||||
|
||||
report_years_subq = (
|
||||
select(RfProjectReport.year)
|
||||
select(RfProjectYear.year)
|
||||
.select_from(RfProjectReport)
|
||||
.join(RfProjectYear, RfProjectYear.id == RfProjectReport.rf_project_year_id)
|
||||
.where(RfProjectReport.project_id == Project.id)
|
||||
.distinct()
|
||||
.scalar_subquery()
|
||||
@ -101,6 +109,88 @@ class ProjectRepository:
|
||||
total = int((await self.db.execute(count_query)).scalar() or 0)
|
||||
return total, payload
|
||||
|
||||
@staticmethod
|
||||
def _serialize_project_with_reports(project: Project, years: list[tuple[RfProjectYear, Smeta | None]]) -> dict:
|
||||
return {
|
||||
"id": project.id,
|
||||
"name": project.name,
|
||||
"level": project.level,
|
||||
"parent_id": project.parent_id,
|
||||
"project_type": project.project_type,
|
||||
"vsp_format": project.vsp_format,
|
||||
"placement_type": placement_type_from_db(project.placement_type),
|
||||
"object_address": project.object_address,
|
||||
"staff_count": project.staff_count,
|
||||
"total_area": float(project.total_area) if project.total_area is not None else None,
|
||||
"org_unit_id": project.org_unit_id,
|
||||
"status": project.status,
|
||||
"technical_number": project.technical_number,
|
||||
"krf_decision_date": project.krf_decision_date.isoformat() if project.krf_decision_date else None,
|
||||
"fk_decision_date": project.fk_decision_date.isoformat() if project.fk_decision_date else None,
|
||||
"board_decision_date": project.board_decision_date.isoformat() if project.board_decision_date else None,
|
||||
"open_relocate_close_date": project.open_relocate_close_date.isoformat() if project.open_relocate_close_date else None,
|
||||
"funding_by_ko_decision": project.funding_by_ko_decision,
|
||||
"created_at": project.created_at.isoformat() if project.created_at else None,
|
||||
"sub_rows": [
|
||||
{
|
||||
"id": rpy.id,
|
||||
"year": rpy.year,
|
||||
"project": f"Смета_{rpy.year}",
|
||||
"is_in_plan": smeta.is_in_plan if smeta else None,
|
||||
"is_in_plan_q2": smeta.is_in_plan_q2 if smeta else None,
|
||||
"is_in_plan_q3": smeta.is_in_plan_q3 if smeta else None,
|
||||
"is_in_plan_q4": smeta.is_in_plan_q4 if smeta else None,
|
||||
"development_block": smeta.development_block if smeta else None,
|
||||
"reserve_to_prrs_ahr": float(smeta.reserve_to_prrs_ahr) if smeta and smeta.reserve_to_prrs_ahr is not None else None,
|
||||
"reserve_to_prrs_kv": float(smeta.reserve_to_prrs_kv) if smeta and smeta.reserve_to_prrs_kv is not None else None,
|
||||
}
|
||||
for rpy, smeta in years
|
||||
],
|
||||
}
|
||||
|
||||
async def get_with_reports(
|
||||
self,
|
||||
year: int | None = None,
|
||||
branch_id: int | None = None,
|
||||
offset: int | None = None,
|
||||
limit: int | None = None,
|
||||
with_count: bool = False,
|
||||
org_unit_ids: list[int] | None = None,
|
||||
) -> list[dict] | tuple[int, list[dict]]:
|
||||
project_query = select(Project).order_by(Project.id)
|
||||
project_query = self._apply_project_filters(project_query, year, branch_id, org_unit_ids)
|
||||
if offset is not None:
|
||||
project_query = project_query.offset(offset)
|
||||
if limit is not None:
|
||||
project_query = project_query.limit(limit)
|
||||
|
||||
projects = (await self.db.execute(project_query)).scalars().all()
|
||||
|
||||
years_by_project: dict[int, list[tuple[RfProjectYear, Smeta | None]]] = {}
|
||||
project_ids = [p.id for p in projects]
|
||||
if project_ids:
|
||||
years_query = (
|
||||
select(RfProjectYear, Smeta)
|
||||
.outerjoin(Smeta, Smeta.rf_project_year_id == RfProjectYear.id)
|
||||
.where(RfProjectYear.project_id.in_(project_ids))
|
||||
.order_by(RfProjectYear.year)
|
||||
)
|
||||
for rpy, smeta in (await self.db.execute(years_query)).all():
|
||||
years_by_project.setdefault(rpy.project_id, []).append((rpy, smeta))
|
||||
|
||||
payload = [
|
||||
self._serialize_project_with_reports(p, years_by_project.get(p.id, []))
|
||||
for p in projects
|
||||
]
|
||||
|
||||
if not with_count:
|
||||
return payload
|
||||
|
||||
count_query = select(func.count(Project.id))
|
||||
count_query = self._apply_project_filters(count_query, year, branch_id, org_unit_ids)
|
||||
total = int((await self.db.execute(count_query)).scalar() or 0)
|
||||
return total, payload
|
||||
|
||||
async def get(
|
||||
self,
|
||||
project_id: int,
|
||||
@ -112,7 +202,9 @@ class ProjectRepository:
|
||||
.scalar_subquery()
|
||||
)
|
||||
report_years_subq = (
|
||||
select(RfProjectReport.year)
|
||||
select(RfProjectYear.year)
|
||||
.select_from(RfProjectReport)
|
||||
.join(RfProjectYear, RfProjectYear.id == RfProjectReport.rf_project_year_id)
|
||||
.where(RfProjectReport.project_id == Project.id)
|
||||
.distinct()
|
||||
.scalar_subquery()
|
||||
@ -162,7 +254,7 @@ class ProjectRepository:
|
||||
query = (
|
||||
select(
|
||||
RfProjectReport.id,
|
||||
RfProjectReport.year,
|
||||
RfProjectYear.year,
|
||||
RfProjectReport.report_type,
|
||||
RfProjectReport.created_by,
|
||||
RfProjectReport.created_at,
|
||||
@ -170,8 +262,9 @@ class ProjectRepository:
|
||||
RfProjectReport.updated_at,
|
||||
line_count_subq.label("line_count"),
|
||||
)
|
||||
.join(RfProjectYear, RfProjectYear.id == RfProjectReport.rf_project_year_id)
|
||||
.where(RfProjectReport.project_id == project_id)
|
||||
.order_by(RfProjectReport.year, RfProjectReport.report_type)
|
||||
.order_by(RfProjectYear.year, RfProjectReport.report_type)
|
||||
)
|
||||
rows = (await self.db.execute(query)).all()
|
||||
reports: list[dict] = []
|
||||
@ -200,9 +293,10 @@ class ProjectRepository:
|
||||
query = (
|
||||
select(RfProjectReport.id)
|
||||
.join(Project, Project.id == RfProjectReport.project_id)
|
||||
.join(RfProjectYear, RfProjectYear.id == RfProjectReport.rf_project_year_id)
|
||||
.where(
|
||||
Project.id == project_id,
|
||||
RfProjectReport.year == year,
|
||||
RfProjectYear.year == year,
|
||||
RfProjectReport.report_type == report_type,
|
||||
)
|
||||
)
|
||||
@ -326,23 +420,120 @@ class ProjectRepository:
|
||||
result = (await self.db.execute(query, {"project_id": project_id})).scalar_one()
|
||||
return result
|
||||
|
||||
async def upd_project(self, project_id: int, column: str, value):
|
||||
query = text(
|
||||
"""
|
||||
SELECT v3.upd_project(CAST(:project_id AS INT), CAST(:column AS TEXT), CAST(:value AS JSONB))
|
||||
"""
|
||||
async def upd_project(self, project_id: int, data: dict) -> None:
|
||||
await self.db.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT v3.upd_project(
|
||||
CAST(:project_id AS INT),
|
||||
CAST(:name AS VARCHAR),
|
||||
CAST(:status AS VARCHAR),
|
||||
CAST(:technical_number AS VARCHAR),
|
||||
CAST(:project_type AS VARCHAR),
|
||||
CAST(:vsp_format AS VARCHAR),
|
||||
CAST(:placement_type AS VARCHAR),
|
||||
CAST(:object_address AS VARCHAR),
|
||||
CAST(:staff_count AS INT),
|
||||
CAST(:total_area AS NUMERIC),
|
||||
CAST(:org_unit_id AS INT),
|
||||
CAST(:krf_decision_date AS DATE),
|
||||
CAST(:fk_decision_date AS DATE),
|
||||
CAST(:board_decision_date AS DATE),
|
||||
CAST(:open_relocate_close_date AS DATE),
|
||||
CAST(:funding_by_ko_decision AS VARCHAR)
|
||||
)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"project_id": project_id,
|
||||
"name": data.get("name"),
|
||||
"status": data.get("status"),
|
||||
"technical_number": data.get("technical_number"),
|
||||
"project_type": data.get("project_type"),
|
||||
"vsp_format": data.get("vsp_format"),
|
||||
"placement_type": data.get("placement_type"),
|
||||
"object_address": data.get("object_address"),
|
||||
"staff_count": data.get("staff_count"),
|
||||
"total_area": data.get("total_area"),
|
||||
"org_unit_id": data.get("org_unit_id"),
|
||||
"krf_decision_date": data.get("krf_decision_date"),
|
||||
"fk_decision_date": data.get("fk_decision_date"),
|
||||
"board_decision_date": data.get("board_decision_date"),
|
||||
"open_relocate_close_date": data.get("open_relocate_close_date"),
|
||||
"funding_by_ko_decision": data.get("funding_by_ko_decision"),
|
||||
},
|
||||
)
|
||||
result = (
|
||||
await self.db.flush()
|
||||
|
||||
async def resolve_rf_project_year_id(
|
||||
self,
|
||||
project_id: int,
|
||||
year: int,
|
||||
org_unit_ids: list[int] | None = None,
|
||||
) -> int | None:
|
||||
query = (
|
||||
select(RfProjectYear.id)
|
||||
.join(Project, Project.id == RfProjectYear.project_id)
|
||||
.where(Project.id == project_id, RfProjectYear.year == year)
|
||||
)
|
||||
if org_unit_ids is not None:
|
||||
if len(org_unit_ids) == 0:
|
||||
return None
|
||||
query = query.where(Project.org_unit_id.in_(org_unit_ids))
|
||||
return (await self.db.execute(query)).scalar_one_or_none()
|
||||
|
||||
async def upd_smeta(self, rf_project_year_id: int, data: dict) -> None:
|
||||
await self.db.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT v3.upd_smeta(
|
||||
CAST(:rf_project_year_id AS INT),
|
||||
CAST(:is_in_plan AS BOOLEAN),
|
||||
CAST(:is_in_plan_q2 AS BOOLEAN),
|
||||
CAST(:is_in_plan_q3 AS BOOLEAN),
|
||||
CAST(:is_in_plan_q4 AS BOOLEAN),
|
||||
CAST(:development_block AS VARCHAR),
|
||||
CAST(:reserve_to_prrs_ahr AS NUMERIC),
|
||||
CAST(:reserve_to_prrs_kv AS NUMERIC)
|
||||
)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"rf_project_year_id": rf_project_year_id,
|
||||
"is_in_plan": data.get("is_in_plan"),
|
||||
"is_in_plan_q2": data.get("is_in_plan_q2"),
|
||||
"is_in_plan_q3": data.get("is_in_plan_q3"),
|
||||
"is_in_plan_q4": data.get("is_in_plan_q4"),
|
||||
"development_block": data.get("development_block"),
|
||||
"reserve_to_prrs_ahr": data.get("reserve_to_prrs_ahr"),
|
||||
"reserve_to_prrs_kv": data.get("reserve_to_prrs_kv"),
|
||||
},
|
||||
)
|
||||
await self.db.flush()
|
||||
|
||||
@staticmethod
|
||||
def _serialize_smeta(smeta: Smeta | None) -> dict | None:
|
||||
if smeta is None:
|
||||
return None
|
||||
return {
|
||||
"id": smeta.id,
|
||||
"rf_project_year_id": smeta.rf_project_year_id,
|
||||
"is_in_plan": smeta.is_in_plan,
|
||||
"is_in_plan_q2": smeta.is_in_plan_q2,
|
||||
"is_in_plan_q3": smeta.is_in_plan_q3,
|
||||
"is_in_plan_q4": smeta.is_in_plan_q4,
|
||||
"development_block": smeta.development_block,
|
||||
"reserve_to_prrs_ahr": float(smeta.reserve_to_prrs_ahr) if smeta.reserve_to_prrs_ahr is not None else None,
|
||||
"reserve_to_prrs_kv": float(smeta.reserve_to_prrs_kv) if smeta.reserve_to_prrs_kv is not None else None,
|
||||
}
|
||||
|
||||
async def get_smeta(self, rf_project_year_id: int) -> dict | None:
|
||||
smeta = (
|
||||
await self.db.execute(
|
||||
query,
|
||||
{
|
||||
"project_id": project_id,
|
||||
"column": column,
|
||||
"value": json.dumps(value),
|
||||
},
|
||||
select(Smeta).where(Smeta.rf_project_year_id == rf_project_year_id)
|
||||
)
|
||||
).scalar_one()
|
||||
return result
|
||||
).scalar_one_or_none()
|
||||
return self._serialize_smeta(smeta)
|
||||
|
||||
async def add_project(
|
||||
self,
|
||||
|
||||
@ -3,6 +3,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from src.db.models.rf_project_report import RfProjectReport
|
||||
from src.db.models.rf_project_year import RfProjectYear
|
||||
|
||||
|
||||
|
||||
@ -25,8 +26,10 @@ class RfProjectReportRepository:
|
||||
if report_id is not None:
|
||||
query = query.where(RfProjectReport.id == report_id)
|
||||
else:
|
||||
query = query.where(
|
||||
RfProjectReport.year == year,
|
||||
query = query.join(
|
||||
RfProjectYear, RfProjectYear.id == RfProjectReport.rf_project_year_id
|
||||
).where(
|
||||
RfProjectYear.year == year,
|
||||
RfProjectReport.report_type == report_type,
|
||||
RfProjectReport.project_id == project_id
|
||||
)
|
||||
@ -42,14 +45,19 @@ class RfProjectReportRepository:
|
||||
year: int | None = None,
|
||||
report_type: str | None = None,
|
||||
project_id: int | None = None,
|
||||
load_year: bool = False
|
||||
) -> list[RfProjectReport]:
|
||||
query = select(
|
||||
RfProjectReport
|
||||
)
|
||||
if report_id is not None:
|
||||
query = query.where(RfProjectReport.id == report_id)
|
||||
if load_year or year:
|
||||
query = query.options(
|
||||
joinedload(RfProjectReport.rf_project_year)
|
||||
)
|
||||
if year is not None:
|
||||
query = query.where(RfProjectReport.year == year)
|
||||
query = query.where(RfProjectYear.year == year)
|
||||
if report_type is not None:
|
||||
query = query.where(RfProjectReport.report_type == report_type)
|
||||
if project_id is not None:
|
||||
|
||||
@ -4,6 +4,7 @@ from src.db.models.project import Project
|
||||
from src.db.models.app_user import AppUser
|
||||
from src.db.models.role import UserRoleEnum
|
||||
from src.core.errors import AccessDeniedException, ValidationException
|
||||
from src.domain.schemas import placement_type_to_db
|
||||
from src.repository.project_repository import ProjectRepository
|
||||
from src.repository.user_repository import UserRepository
|
||||
|
||||
@ -34,6 +35,25 @@ class ProjectService:
|
||||
org_unit_ids=org_unit_ids,
|
||||
)
|
||||
|
||||
async def get_list_with_reports(
|
||||
self,
|
||||
user: AppUser,
|
||||
year: int | None = None,
|
||||
branch_id: int | None = None,
|
||||
offset: int | None = None,
|
||||
limit: int | None = None,
|
||||
with_count: bool = False,
|
||||
) -> list[dict] | tuple[int, list[dict]]:
|
||||
org_unit_ids = await self._allowed_org_unit_ids(user)
|
||||
return await self.project_repo.get_with_reports(
|
||||
year=year,
|
||||
branch_id=branch_id,
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
with_count=with_count,
|
||||
org_unit_ids=org_unit_ids,
|
||||
)
|
||||
|
||||
async def get(self, project_id: int, user: AppUser) -> dict | None:
|
||||
org_unit_ids = await self._allowed_org_unit_ids(user)
|
||||
project = await self.project_repo.get(project_id=project_id, org_unit_ids=org_unit_ids)
|
||||
@ -156,14 +176,34 @@ class ProjectService:
|
||||
async def upd_project(
|
||||
self,
|
||||
project_id: int,
|
||||
column: str,
|
||||
value,
|
||||
data: dict,
|
||||
user: AppUser,
|
||||
):
|
||||
project = await self.get(project_id, user)
|
||||
if not project:
|
||||
raise ValidationException("Проект не найден")
|
||||
return await self.project_repo.upd_project(project_id, column, value)
|
||||
if "placement_type" in data:
|
||||
data["placement_type"] = placement_type_to_db(data["placement_type"])
|
||||
await self.project_repo.upd_project(project_id, data)
|
||||
return await self.get(project_id, user)
|
||||
|
||||
async def upd_smeta(
|
||||
self,
|
||||
project_id: int,
|
||||
year: int,
|
||||
data: dict,
|
||||
user: AppUser,
|
||||
):
|
||||
org_unit_ids = await self._allowed_org_unit_ids(user)
|
||||
rf_project_year_id = await self.project_repo.resolve_rf_project_year_id(
|
||||
project_id=project_id,
|
||||
year=year,
|
||||
org_unit_ids=org_unit_ids,
|
||||
)
|
||||
if rf_project_year_id is None:
|
||||
raise ValidationException("Смета не найдена")
|
||||
await self.project_repo.upd_smeta(rf_project_year_id, data)
|
||||
return await self.project_repo.get_smeta(rf_project_year_id)
|
||||
|
||||
async def add_project(
|
||||
self,
|
||||
@ -187,7 +227,7 @@ class ProjectService:
|
||||
parent_id=parent_id,
|
||||
project_type=project_type,
|
||||
vsp_format=vsp_format,
|
||||
placement_type=placement_type,
|
||||
placement_type=placement_type_to_db(placement_type),
|
||||
object_address=object_address,
|
||||
staff_count=staff_count,
|
||||
total_area=total_area,
|
||||
|
||||
@ -36,10 +36,12 @@ class RfProjectReportService:
|
||||
year: int | None = None,
|
||||
report_type: str | None = None,
|
||||
project_id: int | None = None,
|
||||
load_year: bool = False,
|
||||
) -> list[RfProjectReport]:
|
||||
return await self.pr_repo.get_list(
|
||||
report_id=report_id,
|
||||
year=year,
|
||||
report_type=report_type,
|
||||
project_id=project_id,
|
||||
load_year=load_year,
|
||||
)
|
||||
|
||||
@ -184,6 +184,14 @@ def test_form3_phases_list_filter_smoke(client, admin_tokens):
|
||||
assert "count" in payload
|
||||
assert payload["count"] <= count
|
||||
|
||||
response = client.get(
|
||||
"/api/v1/stages/project/2?&year=2026", headers=_auth_headers(admin_tokens)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert "count" in payload
|
||||
assert payload["count"] <= count
|
||||
|
||||
|
||||
def test_form3_phases_create_forbidden(client, isp_tokens):
|
||||
response = client.post(
|
||||
|
||||
@ -79,7 +79,7 @@ def test_projects_write_smoke(client, admin_tokens, auth_headers):
|
||||
|
||||
upd_project_response = client.patch(
|
||||
f"/api/v1/project/{project_id}",
|
||||
json={"column": "name", "value": f"{name}_U"},
|
||||
json={"name": f"{name}_U"},
|
||||
headers=auth_headers(admin_tokens),
|
||||
)
|
||||
assert upd_project_response.status_code == 200
|
||||
@ -248,3 +248,92 @@ def test_delete_seed_project_smoke(client, admin_tokens, auth_headers):
|
||||
payload = response.json()
|
||||
assert payload["success"] is True
|
||||
assert payload["message"] == "Проект удалён"
|
||||
|
||||
|
||||
def _get_project_with_reports(client, auth_headers, admin_tokens, project_id):
|
||||
response = client.get(
|
||||
"/api/v1/projects/with-reports?limit=1000",
|
||||
headers=auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
return next((p for p in response.json()["result"] if p["id"] == project_id), None)
|
||||
|
||||
|
||||
def test_upd_project_smoke(client, admin_tokens, auth_headers):
|
||||
project_id, name = _create_project(client, admin_tokens, auth_headers)
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/project/{project_id}",
|
||||
json={
|
||||
"name": f"{name}_U",
|
||||
"status": "agreed",
|
||||
"technical_number": "2606_180801",
|
||||
"krf_decision_date": "2026-06-11",
|
||||
"fk_decision_date": "2026-06-18",
|
||||
"board_decision_date": "2026-07-02",
|
||||
"open_relocate_close_date": "2026-07-15",
|
||||
"funding_by_ko_decision": "prrs_budget",
|
||||
"placement_type": "Собственность",
|
||||
},
|
||||
headers=auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
project = _get_project_with_reports(client, auth_headers, admin_tokens, project_id)
|
||||
assert project is not None
|
||||
assert project["name"] == f"{name}_U"
|
||||
assert project["status"] == "agreed"
|
||||
assert project["technical_number"] == "2606_180801"
|
||||
assert project["krf_decision_date"] == "2026-06-11"
|
||||
assert project["fk_decision_date"] == "2026-06-18"
|
||||
assert project["board_decision_date"] == "2026-07-02"
|
||||
assert project["open_relocate_close_date"] == "2026-07-15"
|
||||
assert project["funding_by_ko_decision"] == "prrs_budget"
|
||||
# placement_type сконвертирован в БД-код и обратно в строковое представление
|
||||
assert project["placement_type"] == "Собственность"
|
||||
|
||||
|
||||
def test_upd_smeta_smoke(client, admin_tokens, auth_headers):
|
||||
project_id, _ = _create_project(client, admin_tokens, auth_headers, year=2026)
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/projects/{project_id}/smeta/2026",
|
||||
json={
|
||||
"is_in_plan": True,
|
||||
"is_in_plan_q2": True,
|
||||
"is_in_plan_q3": False,
|
||||
"is_in_plan_q4": False,
|
||||
"development_block": "out_of_base_plan",
|
||||
"reserve_to_prrs_ahr": 1500.5,
|
||||
"reserve_to_prrs_kv": 2300,
|
||||
},
|
||||
headers=auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["is_in_plan"] is True
|
||||
assert payload["is_in_plan_q2"] is True
|
||||
assert payload["is_in_plan_q3"] is False
|
||||
assert payload["is_in_plan_q4"] is False
|
||||
assert payload["development_block"] == "out_of_base_plan"
|
||||
assert payload["reserve_to_prrs_ahr"] == 1500.5
|
||||
assert payload["reserve_to_prrs_kv"] == 2300
|
||||
|
||||
project = _get_project_with_reports(client, auth_headers, admin_tokens, project_id)
|
||||
assert project is not None
|
||||
sub_row = next((sr for sr in project["sub_rows"] if sr["year"] == 2026), None)
|
||||
assert sub_row is not None
|
||||
assert sub_row["is_in_plan"] is True
|
||||
assert sub_row["development_block"] == "out_of_base_plan"
|
||||
|
||||
|
||||
def test_upd_smeta_not_found(client, admin_tokens, auth_headers):
|
||||
project_id, _ = _create_project(client, admin_tokens, auth_headers, year=2026)
|
||||
response = client.patch(
|
||||
f"/api/v1/projects/{project_id}/smeta/2099",
|
||||
json={"is_in_plan": True},
|
||||
headers=auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 400
|
||||
payload = response.json()
|
||||
assert payload.get("message") == "Смета не найдена"
|
||||
|
||||
@ -34,13 +34,21 @@ INSERT INTO v3.vsp (id,branch_id,reg_number,address,format,opened_at,placement_t
|
||||
(3,2,'3434','победы 12','формат','2026-06-01','субаренда', NULL,34,NULL,true,'2026-07-01 13:32:35.327097',false,'2026-06-29 15:53:50.236062',1,'3434',NULL,'2','примечание','567','2027-07-01');
|
||||
SELECT setval('v3.vsp_id_seq', 3);
|
||||
|
||||
INSERT INTO v3.project ("id","name","level",parent_id,project_type,vsp_format,placement_type,object_address,staff_count,total_area,org_unit_id) VALUES
|
||||
(2,'PT6d112310_U','project',NULL,NULL,NULL,NULL,NULL,NULL,NULL,1);
|
||||
INSERT INTO v3.project ("id","name","level",parent_id,project_type,vsp_format,placement_type,object_address,staff_count,total_area,org_unit_id,status,technical_number,krf_decision_date,fk_decision_date,board_decision_date,open_relocate_close_date,funding_by_ko_decision) VALUES
|
||||
(2,'PT6d112310_U','project',NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,'created','2606_180801','2026-06-11','2026-06-18','2026-07-02','2026-07-15','prrs_budget');
|
||||
SELECT setval('v3.project_id_seq', 3);
|
||||
|
||||
INSERT INTO v3.rf_project_report (id,project_id,"year",report_type,created_by,created_at,updated_by,updated_at) VALUES
|
||||
(1,2,2026,'LIMIT',NULL,'2026-05-12 12:43:56.20138',NULL,'2026-05-12 12:43:56.20138'),
|
||||
(2,2,2026,'CURRENT_EXPENSES',NULL,'2026-05-12 12:43:56.20138',NULL,'2026-05-12 12:43:56.20138');
|
||||
INSERT INTO v3.rf_project_year ("id",project_id,"year") VALUES
|
||||
(1,2,2026);
|
||||
SELECT setval('v3.rf_project_year_id_seq', 1);
|
||||
|
||||
INSERT INTO v3.smeta ("id",rf_project_year_id,is_in_plan,is_in_plan_q2,is_in_plan_q3,is_in_plan_q4,development_block,reserve_to_prrs_ahr,reserve_to_prrs_kv) VALUES
|
||||
(1,1,true,false,false,false,'out_of_base_plan',1500.00,2300.00);
|
||||
SELECT setval('v3.smeta_id_seq', 1);
|
||||
|
||||
INSERT INTO v3.rf_project_report (id,project_id,rf_project_year_id,report_type,created_by,created_at,updated_by,updated_at) VALUES
|
||||
(1,2,1,'LIMIT',NULL,'2026-05-12 12:43:56.20138',NULL,'2026-05-12 12:43:56.20138'),
|
||||
(2,2,1,'CURRENT_EXPENSES',NULL,'2026-05-12 12:43:56.20138',NULL,'2026-05-12 12:43:56.20138');
|
||||
SELECT setval('v3.rf_project_report_id_seq', 3);
|
||||
|
||||
INSERT INTO v3.form3_phase (rf_project_report_id,phase_code,"role",column_keys,opens_at,closes_at) VALUES
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user