form-3-phases-timezones: редактирвоание этапов для 3 формы, таймзоны для этапов 3 формы, поправил entity_type для этапов
This commit is contained in:
parent
79bf3231b9
commit
6ef133e728
785
api/alembic/versions/0009_phase3_timestamps.py
Normal file
785
api/alembic/versions/0009_phase3_timestamps.py
Normal file
@ -0,0 +1,785 @@
|
|||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision = "0009"
|
||||||
|
down_revision = "0008"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
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);
|
||||||
|
BEGIN
|
||||||
|
SELECT report_type, org_unit_id INTO v_report_type, v_org_unit
|
||||||
|
FROM v3.rf_project_report AS rpr
|
||||||
|
INNER JOIN v3.project AS p ON p.id = rpr.project_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::text || v_utc_offset)::timestamptz,
|
||||||
|
(pt.closes_at::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$
|
||||||
|
;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
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, rpr.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
|
||||||
|
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$
|
||||||
|
;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
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, rpr.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
|
||||||
|
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$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
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, rpr.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
|
||||||
|
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$
|
||||||
|
;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION v3.add_project(p_name character varying, p_year integer, p_org_unit_id integer, p_level character varying DEFAULT 'project'::character varying, p_parent_id integer DEFAULT NULL::integer, p_project_type character varying DEFAULT NULL::character varying, p_vsp_format character varying DEFAULT NULL::character varying, p_placement_type character varying DEFAULT NULL::character varying, p_object_address character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_system_code character varying DEFAULT NULL::character varying)
|
||||||
|
RETURNS TABLE(project_id integer, limit_report_id integer, current_expenses_report_id integer)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_pid INT;
|
||||||
|
v_lim INT;
|
||||||
|
v_cur INT;
|
||||||
|
BEGIN
|
||||||
|
IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
|
||||||
|
RAISE EXCEPTION 'p_name обязателен';
|
||||||
|
END IF;
|
||||||
|
IF p_year IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_year обязателен';
|
||||||
|
END IF;
|
||||||
|
IF p_org_unit_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_org_unit_id обязателен';
|
||||||
|
END IF;
|
||||||
|
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;
|
||||||
|
IF p_parent_id IS NOT NULL AND
|
||||||
|
NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_parent_id AND level = 'program') THEN
|
||||||
|
RAISE EXCEPTION 'parent project id=% не существует или не имеет level=program', p_parent_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- INSERT project (CHECK-ы name/project_type/vsp_format/placement_type
|
||||||
|
-- из migrate_form3_v1.sql применятся автоматически).
|
||||||
|
INSERT INTO v3.project (
|
||||||
|
name, level, parent_id, org_unit_id,
|
||||||
|
project_type, vsp_format, placement_type, object_address, staff_count, total_area,
|
||||||
|
system_code
|
||||||
|
) VALUES (
|
||||||
|
p_name, p_level, p_parent_id, p_org_unit_id,
|
||||||
|
p_project_type, p_vsp_format, p_placement_type, p_object_address, p_staff_count, p_total_area,
|
||||||
|
p_system_code
|
||||||
|
) RETURNING id INTO v_pid;
|
||||||
|
|
||||||
|
-- Первый год: 2 отчёта (LIMIT + CURRENT_EXPENSES) + snapshot фаз + сетка статей.
|
||||||
|
-- p_emit_audit=false — событие года покрыто PROJECT_CREATE ниже. Дальнейшие
|
||||||
|
-- годы добавляются отдельно через v3.add_project_year.
|
||||||
|
SELECT y.limit_report_id, y.current_expenses_report_id
|
||||||
|
INTO v_lim, v_cur
|
||||||
|
FROM v3.add_project_year(v_pid, p_year, false) y;
|
||||||
|
|
||||||
|
-- Аудит: PROJECT_CREATE — все заполненные поля + id отчётов
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'PROJECT_CREATE', 'PROJECT',
|
||||||
|
jsonb_strip_nulls(jsonb_build_object(
|
||||||
|
'project_id', v_pid,
|
||||||
|
'name', p_name,
|
||||||
|
'year', p_year,
|
||||||
|
'level', p_level,
|
||||||
|
'parent_id', p_parent_id,
|
||||||
|
'org_unit_id', p_org_unit_id,
|
||||||
|
'project_type', p_project_type,
|
||||||
|
'vsp_format', p_vsp_format,
|
||||||
|
'placement_type', p_placement_type,
|
||||||
|
'object_address', p_object_address,
|
||||||
|
'system_code', p_system_code,
|
||||||
|
'staff_count', p_staff_count,
|
||||||
|
'total_area', p_total_area,
|
||||||
|
'limit_report_id', v_lim,
|
||||||
|
'current_expenses_report_id', v_cur
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN QUERY SELECT v_pid, v_lim, v_cur;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION v3.upd_phases_timezones(p_org_unit_id integer, p_old_tz character varying, p_new_tz character varying)
|
||||||
|
RETURNS integer
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_result integer;
|
||||||
|
v_result3 integer;
|
||||||
|
BEGIN
|
||||||
|
IF p_old_tz = p_new_tz THEN
|
||||||
|
RETURN 0;
|
||||||
|
END IF;
|
||||||
|
WITH updated AS (
|
||||||
|
UPDATE v3.form_phase
|
||||||
|
SET opens_at = ((opens_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz,
|
||||||
|
closes_at = ((closes_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz
|
||||||
|
WHERE budget_form_id IN (SELECT id FROM v3.budget_form WHERE org_unit_id = p_org_unit_id)
|
||||||
|
AND (
|
||||||
|
opens_at >= CURRENT_TIMESTAMP
|
||||||
|
OR closes_at >= CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
RETURNING *
|
||||||
|
)
|
||||||
|
SELECT COUNT(*) INTO v_result FROM updated;
|
||||||
|
|
||||||
|
WITH updated AS (
|
||||||
|
UPDATE v3.form3_phase
|
||||||
|
SET opens_at = ((opens_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz,
|
||||||
|
closes_at = ((closes_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz
|
||||||
|
WHERE rf_project_report_id IN (
|
||||||
|
SELECT id FROM V3.rf_project_report rpr WHERE project_id IN (
|
||||||
|
SELECT id FROM v3.project WHERE org_unit_id = p_org_unit_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
opens_at >= CURRENT_TIMESTAMP
|
||||||
|
OR closes_at >= CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
RETURNING *
|
||||||
|
)
|
||||||
|
SELECT COUNT(*) INTO v_result3 FROM updated;
|
||||||
|
RETURN v_result + v_result3;
|
||||||
|
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION v3.add_form_phase(p_budget_form_id integer, p_sheet character varying, 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.form_phase
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_phase v3.form_phase;
|
||||||
|
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);
|
||||||
|
BEGIN
|
||||||
|
SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year
|
||||||
|
INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year
|
||||||
|
FROM v3.budget_form bf
|
||||||
|
LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
|
||||||
|
WHERE bf.id = p_budget_form_id;
|
||||||
|
|
||||||
|
SELECT utc_offset INTO v_utc_offset
|
||||||
|
FROM v3.org_unit
|
||||||
|
WHERE id = v_org_unit_id;
|
||||||
|
|
||||||
|
INSERT INTO v3.form_phase(
|
||||||
|
budget_form_id, sheet, phase_code, role, column_keys, opens_at, closes_at
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
p_budget_form_id,
|
||||||
|
p_sheet,
|
||||||
|
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(
|
||||||
|
COALESCE(v_form_type_code, ''),
|
||||||
|
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:%s', p_budget_form_id, p_sheet, p_phase_code);
|
||||||
|
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
||||||
|
jsonb_build_object(
|
||||||
|
'entity_type', 'form',
|
||||||
|
'entity_id', p_budget_form_id,
|
||||||
|
'core_entity_type', 'form_phase',
|
||||||
|
'core_entity_id', v_phase_id,
|
||||||
|
'action', 'create',
|
||||||
|
'phase_id', v_phase_id,
|
||||||
|
'phase_name', p_phase_code,
|
||||||
|
'task_id', p_budget_form_id,
|
||||||
|
'form_id', p_budget_form_id,
|
||||||
|
'budget_form_id', p_budget_form_id,
|
||||||
|
'org_unit_id', v_org_unit_id,
|
||||||
|
'org_unit_name', v_org_unit_title,
|
||||||
|
'sheet', p_sheet,
|
||||||
|
'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
|
||||||
|
),
|
||||||
|
p_budget_form_id, null, v_org_unit_id
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN v_phase;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION v3.del_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying)
|
||||||
|
RETURNS boolean
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_old v3.form_phase;
|
||||||
|
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;
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM v3.form_phase
|
||||||
|
WHERE budget_form_id = p_budget_form_id
|
||||||
|
AND sheet = p_sheet
|
||||||
|
AND phase_code = p_phase_code
|
||||||
|
RETURNING * INTO v_old;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RETURN FALSE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year
|
||||||
|
INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year
|
||||||
|
FROM v3.budget_form bf
|
||||||
|
LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
|
||||||
|
WHERE bf.id = p_budget_form_id;
|
||||||
|
v_task_name := trim(concat(
|
||||||
|
COALESCE(v_form_type_code, ''),
|
||||||
|
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:%s', p_budget_form_id, p_sheet, p_phase_code);
|
||||||
|
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
||||||
|
jsonb_build_object(
|
||||||
|
'entity_type', 'form',
|
||||||
|
'entity_id', p_budget_form_id,
|
||||||
|
'core_entity_type', 'form_phase',
|
||||||
|
'core_entity_id', v_phase_id,
|
||||||
|
'action', 'delete',
|
||||||
|
'phase_id', v_phase_id,
|
||||||
|
'phase_name', p_phase_code,
|
||||||
|
'task_id', p_budget_form_id,
|
||||||
|
'task_name', NULLIF(v_task_name, ''),
|
||||||
|
'form_id', p_budget_form_id,
|
||||||
|
'budget_form_id', p_budget_form_id,
|
||||||
|
'org_unit_id', v_org_unit_id,
|
||||||
|
'org_unit_name', v_org_unit_title,
|
||||||
|
'sheet', p_sheet,
|
||||||
|
'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
|
||||||
|
),
|
||||||
|
p_budget_form_id, null, v_org_unit_id
|
||||||
|
);
|
||||||
|
RETURN TRUE;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION v3.upd_form_phase(p_budget_form_id integer, p_sheet character varying, 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.form_phase
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_old v3.form_phase;
|
||||||
|
v_new v3.form_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;
|
||||||
|
BEGIN
|
||||||
|
SELECT * INTO v_old
|
||||||
|
FROM v3.form_phase
|
||||||
|
WHERE budget_form_id = p_budget_form_id
|
||||||
|
AND sheet = p_sheet
|
||||||
|
AND phase_code = p_phase_code;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'form_phase не найден (form_id=%, sheet=%, phase=%)',
|
||||||
|
p_budget_form_id, p_sheet, p_phase_code;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year, ou.utc_offset
|
||||||
|
INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year, v_utc_offset
|
||||||
|
FROM v3.budget_form bf
|
||||||
|
LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
|
||||||
|
WHERE bf.id = p_budget_form_id;
|
||||||
|
v_task_name := trim(concat(
|
||||||
|
COALESCE(v_form_type_code, ''),
|
||||||
|
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.form_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 budget_form_id = p_budget_form_id
|
||||||
|
AND sheet = p_sheet
|
||||||
|
AND phase_code = p_phase_code
|
||||||
|
RETURNING * INTO v_new;
|
||||||
|
|
||||||
|
|
||||||
|
v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, 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', 'form',
|
||||||
|
'entity_id', p_budget_form_id,
|
||||||
|
'core_entity_type', 'form_phase',
|
||||||
|
'core_entity_id', v_phase_id,
|
||||||
|
|
||||||
|
'phase_id', v_phase_id,
|
||||||
|
'phase_name', p_phase_code,
|
||||||
|
'task_id', p_budget_form_id,
|
||||||
|
'task_name', NULLIF(v_task_name, ''),
|
||||||
|
'form_id', p_budget_form_id,
|
||||||
|
'budget_form_id', p_budget_form_id,
|
||||||
|
'org_unit_id', v_org_unit_id,
|
||||||
|
'org_unit_name', v_org_unit_title,
|
||||||
|
'sheet', p_sheet,
|
||||||
|
'phase_code', p_phase_code,
|
||||||
|
'closes_at_before', v_old.closes_at,
|
||||||
|
'closes_at_after', v_new.closes_at
|
||||||
|
),
|
||||||
|
p_budget_form_id
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_has_window_change THEN
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
||||||
|
jsonb_build_object(
|
||||||
|
'entity_type', 'form',
|
||||||
|
'entity_id', p_budget_form_id,
|
||||||
|
'core_entity_type', 'form_phase',
|
||||||
|
'core_entity_id', v_phase_id,
|
||||||
|
'action', 'update',
|
||||||
|
'phase_id', v_phase_id,
|
||||||
|
'phase_name', p_phase_code,
|
||||||
|
'task_id', p_budget_form_id,
|
||||||
|
'task_name', NULLIF(v_task_name, ''),
|
||||||
|
'form_id', p_budget_form_id,
|
||||||
|
'budget_form_id', p_budget_form_id,
|
||||||
|
'org_unit_id', v_org_unit_id,
|
||||||
|
'org_unit_name', v_org_unit_title,
|
||||||
|
'sheet', p_sheet,
|
||||||
|
'phase_code', p_phase_code,
|
||||||
|
'changes', v_changes
|
||||||
|
),
|
||||||
|
p_budget_form_id, null, v_org_unit_id
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN v_new;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
@ -3,11 +3,17 @@ import datetime
|
|||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.services.project_service import ProjectService
|
||||||
|
from src.services.form3_phase_service import Form3PhaseService
|
||||||
|
from src.services.rf_project_report_service import RfProjectReportService
|
||||||
from src.services.org_unit_service import OrgUnitService
|
from src.services.org_unit_service import OrgUnitService
|
||||||
from src.db.models.app_user import AppUser
|
from src.db.models.app_user import AppUser
|
||||||
from src.domain.schemas import (
|
from src.domain.schemas import (
|
||||||
BaseListResponse,
|
BaseListResponse,
|
||||||
BaseSingleResponse,
|
BaseSingleResponse,
|
||||||
|
Form3PhaseCreate,
|
||||||
|
Form3PhaseResponse,
|
||||||
|
Form3PhaseUpdate,
|
||||||
FormPhaseCreate,
|
FormPhaseCreate,
|
||||||
FormPhaseResponse,
|
FormPhaseResponse,
|
||||||
FormPhaseUpdate,
|
FormPhaseUpdate,
|
||||||
@ -66,8 +72,6 @@ async def get_form_phases(
|
|||||||
element.closes_at = element.closes_at.astimezone(tz)
|
element.closes_at = element.closes_at.astimezone(tz)
|
||||||
else:
|
else:
|
||||||
result = []
|
result = []
|
||||||
for element in result:
|
|
||||||
pass
|
|
||||||
return BaseListResponse(
|
return BaseListResponse(
|
||||||
result=result,
|
result=result,
|
||||||
count=len(phases),
|
count=len(phases),
|
||||||
@ -168,3 +172,173 @@ async def delete_form_phase(
|
|||||||
)
|
)
|
||||||
if not deleted:
|
if not deleted:
|
||||||
raise HTTPException(404, "Этап не найден")
|
raise HTTPException(404, "Этап не найден")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/project/{project_id}")
|
||||||
|
async def get_form3_phases(
|
||||||
|
project_id: int,
|
||||||
|
year: int | None = None,
|
||||||
|
report_type: str | None = None,
|
||||||
|
phase_code: str | None = None,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||||
|
) -> BaseListResponse[Form3PhaseResponse]:
|
||||||
|
f3p_service = Form3PhaseService(db)
|
||||||
|
report_service = RfProjectReportService(db)
|
||||||
|
project_service = ProjectService(db)
|
||||||
|
org_unit_service = OrgUnitService(db)
|
||||||
|
|
||||||
|
reports = await report_service.get_list(
|
||||||
|
project_id=project_id,
|
||||||
|
year=year,
|
||||||
|
report_type=report_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
phases = await f3p_service.get_list(
|
||||||
|
report_id=[el.id for el in reports],
|
||||||
|
phase_code=phase_code,
|
||||||
|
)
|
||||||
|
if phases:
|
||||||
|
project = await project_service.get_instance(user=current_user, project_id=project_id)
|
||||||
|
org_unit = await org_unit_service.get(user=current_user, org_unit_id=project.org_unit_id)
|
||||||
|
result = [Form3PhaseResponse.model_validate(p) for p in phases]
|
||||||
|
if org_unit.utc_offset:
|
||||||
|
tz = timezone_from_offset(org_unit.utc_offset)
|
||||||
|
for element in result:
|
||||||
|
if element.opens_at:
|
||||||
|
element.opens_at = element.opens_at.astimezone(tz)
|
||||||
|
if element.closes_at:
|
||||||
|
element.closes_at = element.closes_at.astimezone(tz)
|
||||||
|
else:
|
||||||
|
result = []
|
||||||
|
if result:
|
||||||
|
reports = {report.id: report for report in reports}
|
||||||
|
for i, element in enumerate(result):
|
||||||
|
element.year = reports[phases[i].rf_project_report_id].year
|
||||||
|
element.report_type = reports[phases[i].rf_project_report_id].report_type
|
||||||
|
return BaseListResponse(
|
||||||
|
result=result,
|
||||||
|
count=len(phases),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/project/{project_id}")
|
||||||
|
async def create_form3_phase(
|
||||||
|
project_id: int,
|
||||||
|
body: Form3PhaseCreate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(require_admin),
|
||||||
|
) -> BaseSingleResponse[Form3PhaseResponse]:
|
||||||
|
report_service = RfProjectReportService(db)
|
||||||
|
report = await report_service.get(
|
||||||
|
project_id=project_id,
|
||||||
|
report_type=body.report_type,
|
||||||
|
year=body.year,
|
||||||
|
user=current_user,
|
||||||
|
load_project=True,
|
||||||
|
)
|
||||||
|
if not report:
|
||||||
|
raise HTTPException(404, "Форма не найдена")
|
||||||
|
|
||||||
|
org_unit_service = OrgUnitService(db)
|
||||||
|
org_unit = await org_unit_service.get(user=current_user, org_unit_id=report.project.org_unit_id)
|
||||||
|
|
||||||
|
f3p_service = Form3PhaseService(db)
|
||||||
|
phase = await f3p_service.create(
|
||||||
|
report_id=report.id,
|
||||||
|
body=body,
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = Form3PhaseResponse.model_validate(phase)
|
||||||
|
|
||||||
|
if org_unit.utc_offset:
|
||||||
|
tz = timezone_from_offset(org_unit.utc_offset)
|
||||||
|
if result.opens_at:
|
||||||
|
result.opens_at = result.opens_at.astimezone(tz)
|
||||||
|
if result.closes_at:
|
||||||
|
result.closes_at = result.closes_at.astimezone(tz)
|
||||||
|
|
||||||
|
result.year = body.year
|
||||||
|
result.report_type = body.report_type
|
||||||
|
|
||||||
|
return BaseSingleResponse(result=result)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/project/{project_id}/{year}/{report_type}/{phase_code}")
|
||||||
|
async def update_form3_phase(
|
||||||
|
project_id: int,
|
||||||
|
year: int,
|
||||||
|
report_type: str,
|
||||||
|
phase_code: str,
|
||||||
|
body: Form3PhaseUpdate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(require_admin),
|
||||||
|
) -> BaseSingleResponse[Form3PhaseResponse]:
|
||||||
|
report_service = RfProjectReportService(db)
|
||||||
|
report = await report_service.get(
|
||||||
|
project_id=project_id,
|
||||||
|
report_type=report_type,
|
||||||
|
year=year,
|
||||||
|
user=current_user,
|
||||||
|
load_project=True,
|
||||||
|
)
|
||||||
|
if not report:
|
||||||
|
raise HTTPException(404, "Форма не найдена")
|
||||||
|
|
||||||
|
f3p_service = Form3PhaseService(db)
|
||||||
|
phase = await f3p_service.update(
|
||||||
|
report_id=report.id,
|
||||||
|
phase_code=phase_code,
|
||||||
|
body=body,
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not phase:
|
||||||
|
raise HTTPException(404, "Этап не найден")
|
||||||
|
|
||||||
|
org_unit_service = OrgUnitService(db)
|
||||||
|
org_unit = await org_unit_service.get(user=current_user, org_unit_id=report.project.org_unit_id)
|
||||||
|
result = Form3PhaseResponse.model_validate(phase)
|
||||||
|
|
||||||
|
if org_unit.utc_offset:
|
||||||
|
tz = timezone_from_offset(org_unit.utc_offset)
|
||||||
|
if result.opens_at:
|
||||||
|
result.opens_at = result.opens_at.astimezone(tz)
|
||||||
|
if result.closes_at:
|
||||||
|
result.closes_at = result.closes_at.astimezone(tz)
|
||||||
|
|
||||||
|
result.year = year
|
||||||
|
result.report_type = report_type
|
||||||
|
|
||||||
|
return BaseSingleResponse(result=result)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/project/{project_id}/{year}/{report_type}/{phase_code}")
|
||||||
|
async def delete_form3_phase(
|
||||||
|
project_id: int,
|
||||||
|
year: int,
|
||||||
|
report_type: str,
|
||||||
|
phase_code: str,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(require_admin),
|
||||||
|
):
|
||||||
|
report_service = RfProjectReportService(db)
|
||||||
|
report = await report_service.get(
|
||||||
|
project_id=project_id,
|
||||||
|
report_type=report_type,
|
||||||
|
year=year,
|
||||||
|
user=current_user,
|
||||||
|
load_project=True,
|
||||||
|
)
|
||||||
|
if not report:
|
||||||
|
raise HTTPException(404, "Форма не найдена")
|
||||||
|
|
||||||
|
f3p_service = Form3PhaseService(db)
|
||||||
|
deleted = await f3p_service.delete(
|
||||||
|
report_id=report.id,
|
||||||
|
phase_code=phase_code,
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
if not deleted:
|
||||||
|
raise HTTPException(404, "Этап не найден")
|
||||||
|
|||||||
@ -46,4 +46,4 @@ class Project(Base):
|
|||||||
# children: Mapped[list["Project"]] = relationship("Project", back_populates="parent") # type: ignore
|
# children: Mapped[list["Project"]] = relationship("Project", back_populates="parent") # type: ignore
|
||||||
# org_unit = relationship("OrgUnit", back_populates="projects")
|
# org_unit = relationship("OrgUnit", back_populates="projects")
|
||||||
# budget_lines: Mapped[list["BudgetLine"]] = relationship("BudgetLine", back_populates="project") # type: ignore
|
# budget_lines: Mapped[list["BudgetLine"]] = relationship("BudgetLine", back_populates="project") # type: ignore
|
||||||
# rf_project_reports: Mapped[list["RfProjectReport"]] = relationship("RfProjectReport", back_populates="project") # type: ignore
|
rf_project_reports: Mapped[list["RfProjectReport"]] = relationship("RfProjectReport", back_populates="project") # type: ignore
|
||||||
|
|||||||
@ -25,7 +25,7 @@ class RfProjectReport(Base):
|
|||||||
updated_by: Mapped[int | None] = mapped_column(Integer, ForeignKey("v3.app_user.id"))
|
updated_by: Mapped[int | None] = mapped_column(Integer, ForeignKey("v3.app_user.id"))
|
||||||
updated_at: Mapped[datetime | None] = mapped_column(DateTime, server_default=func.now())
|
updated_at: Mapped[datetime | None] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
# project: Mapped[Project] = relationship("Project", back_populates="rf_project_reports")
|
project: Mapped["Project"] = relationship("Project", back_populates="rf_project_reports") # type: ignore
|
||||||
# creator: Mapped[AppUser | None] = relationship("AppUser", foreign_keys=[created_by])
|
# creator: Mapped[AppUser | None] = relationship("AppUser", foreign_keys=[created_by])
|
||||||
# updater: Mapped[AppUser | None] = relationship("AppUser", foreign_keys=[updated_by])
|
# updater: Mapped[AppUser | None] = relationship("AppUser", foreign_keys=[updated_by])
|
||||||
# lines: Mapped[list[RfProjectReportLine]] = relationship("RfProjectReportLine", back_populates="report")
|
# lines: Mapped[list[RfProjectReportLine]] = relationship("RfProjectReportLine", back_populates="report")
|
||||||
|
|||||||
@ -425,6 +425,35 @@ class FormPhaseUpdate(BaseModel):
|
|||||||
closes_at: datetime | None = None
|
closes_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Form3PhaseResponse(BaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
phase_code: str
|
||||||
|
role: str
|
||||||
|
column_keys: list[str]
|
||||||
|
opens_at: datetime
|
||||||
|
closes_at: datetime
|
||||||
|
year: int | None = None
|
||||||
|
report_type: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Form3PhaseCreate(BaseModel):
|
||||||
|
year: int
|
||||||
|
report_type: str
|
||||||
|
phase_code: str
|
||||||
|
role: FormPhaseRole
|
||||||
|
column_keys: list[str]
|
||||||
|
opens_at: datetime
|
||||||
|
closes_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class Form3PhaseUpdate(BaseModel):
|
||||||
|
role: FormPhaseRole | None = None
|
||||||
|
column_keys: list[str] | None = None
|
||||||
|
opens_at: datetime | None = None
|
||||||
|
closes_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
class AuditLogBase(BaseModel):
|
class AuditLogBase(BaseModel):
|
||||||
"""Базовая схема записи аудита (единый формат вывода как у auditlog)."""
|
"""Базовая схема записи аудита (единый формат вывода как у auditlog)."""
|
||||||
|
|
||||||
|
|||||||
116
api/src/repository/form3_phase_repository.py
Normal file
116
api/src/repository/form3_phase_repository.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import select, text
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.form3_phase import Form3Phase
|
||||||
|
|
||||||
|
|
||||||
|
class Form3PhaseRepository:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get_list(
|
||||||
|
self,
|
||||||
|
report_id: int | list[int],
|
||||||
|
phase_code: str | None = None,
|
||||||
|
) -> list[Form3Phase]:
|
||||||
|
if isinstance(report_id, int):
|
||||||
|
query = select(Form3Phase).where(Form3Phase.rf_project_report_id == report_id)
|
||||||
|
else:
|
||||||
|
query = select(Form3Phase).where(Form3Phase.rf_project_report_id.in_(report_id))
|
||||||
|
if phase_code is not None:
|
||||||
|
query = query.where(Form3Phase.phase_code == phase_code)
|
||||||
|
return (await self.db.execute(query)).scalars().all()
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self, report_id: int, phase_code: str
|
||||||
|
) -> Form3Phase | None:
|
||||||
|
query = (
|
||||||
|
select(Form3Phase)
|
||||||
|
.where(Form3Phase.rf_project_report_id == report_id)
|
||||||
|
.where(Form3Phase.phase_code == phase_code)
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
return (await self.db.execute(query)).scalar_one_or_none()
|
||||||
|
|
||||||
|
async def create(
|
||||||
|
self,
|
||||||
|
report_id: int,
|
||||||
|
phase_code: str,
|
||||||
|
role: str,
|
||||||
|
column_keys: list[str],
|
||||||
|
opens_at: datetime,
|
||||||
|
closes_at: datetime,
|
||||||
|
) -> Form3Phase:
|
||||||
|
result = await self.db.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
(v3.add_form3_phase(
|
||||||
|
:report_id,
|
||||||
|
:phase_code,
|
||||||
|
:role,
|
||||||
|
:column_keys,
|
||||||
|
CAST(:opens_at AS TIMESTAMP WITHOUT TIME ZONE),
|
||||||
|
CAST(:closes_at AS TIMESTAMP WITHOUT TIME ZONE)
|
||||||
|
)).rf_project_report_id
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"report_id": report_id,
|
||||||
|
"phase_code": phase_code,
|
||||||
|
"role": role,
|
||||||
|
"column_keys": column_keys,
|
||||||
|
"opens_at": opens_at,
|
||||||
|
"closes_at": closes_at,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if result.scalar_one_or_none() is None:
|
||||||
|
return None
|
||||||
|
return await self.get(report_id, phase_code)
|
||||||
|
|
||||||
|
async def update(
|
||||||
|
self, report_id: int, phase_code: str, data: dict
|
||||||
|
) -> Form3Phase | None:
|
||||||
|
result = await self.db.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
(v3.upd_form3_phase(
|
||||||
|
:report_id,
|
||||||
|
:phase_code,
|
||||||
|
:role,
|
||||||
|
:column_keys,
|
||||||
|
CAST(:opens_at AS TIMESTAMP WITHOUT TIME ZONE),
|
||||||
|
CAST(:closes_at AS TIMESTAMP WITHOUT TIME ZONE)
|
||||||
|
)).rf_project_report_id
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"report_id": report_id,
|
||||||
|
"phase_code": phase_code,
|
||||||
|
"role": data.get("role"),
|
||||||
|
"column_keys": data.get("column_keys"),
|
||||||
|
"opens_at": data.get("opens_at"),
|
||||||
|
"closes_at": data.get("closes_at"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if result.scalar_one_or_none() is None:
|
||||||
|
return None
|
||||||
|
return await self.get(report_id, phase_code)
|
||||||
|
|
||||||
|
async def delete(
|
||||||
|
self, report_id: int, phase_code: str
|
||||||
|
) -> bool:
|
||||||
|
result = await self.db.execute(
|
||||||
|
text(
|
||||||
|
"SELECT v3.del_form3_phase(:report_id, :phase_code)"
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"report_id": report_id,
|
||||||
|
"phase_code": phase_code,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
deleted = result.scalar_one_or_none()
|
||||||
|
return bool(deleted)
|
||||||
@ -134,6 +134,23 @@ class ProjectRepository:
|
|||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
return self._serialize_project(row[0], row[1], row[2], row[3])
|
return self._serialize_project(row[0], row[1], row[2], row[3])
|
||||||
|
|
||||||
|
async def get_instance(
|
||||||
|
self,
|
||||||
|
project_id: int,
|
||||||
|
org_unit_ids: list[int] | None = None,
|
||||||
|
) -> Project | None:
|
||||||
|
query = (
|
||||||
|
select(
|
||||||
|
Project,
|
||||||
|
)
|
||||||
|
.where(Project.id == project_id)
|
||||||
|
)
|
||||||
|
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 get_reports(self, project_id: int) -> list[dict]:
|
async def get_reports(self, project_id: int) -> list[dict]:
|
||||||
line_count_subq = (
|
line_count_subq = (
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
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_report import RfProjectReport
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ class RfProjectReportRepository:
|
|||||||
year: int | None = None,
|
year: int | None = None,
|
||||||
report_type: str | None = None,
|
report_type: str | None = None,
|
||||||
project_id: int | None = None,
|
project_id: int | None = None,
|
||||||
|
load_project: bool = False,
|
||||||
) -> RfProjectReport | None:
|
) -> RfProjectReport | None:
|
||||||
assert report_id is not None or all((el is not None for el in (year, report_type, project_id)))
|
assert report_id is not None or all((el is not None for el in (year, report_type, project_id)))
|
||||||
query = select(
|
query = select(
|
||||||
@ -29,5 +31,28 @@ class RfProjectReportRepository:
|
|||||||
RfProjectReport.project_id == project_id
|
RfProjectReport.project_id == project_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if load_project:
|
||||||
|
query = query.options(joinedload(RfProjectReport.project))
|
||||||
query = query.limit(1)
|
query = query.limit(1)
|
||||||
return (await self.db.execute(query)).scalar_one_or_none()
|
return (await self.db.execute(query)).scalar_one_or_none()
|
||||||
|
|
||||||
|
async def get_list(
|
||||||
|
self,
|
||||||
|
report_id: int | None = None,
|
||||||
|
year: int | None = None,
|
||||||
|
report_type: str | None = None,
|
||||||
|
project_id: int | None = None,
|
||||||
|
) -> list[RfProjectReport]:
|
||||||
|
query = select(
|
||||||
|
RfProjectReport
|
||||||
|
)
|
||||||
|
if report_id is not None:
|
||||||
|
query = query.where(RfProjectReport.id == report_id)
|
||||||
|
if year is not None:
|
||||||
|
query = query.where(RfProjectReport.year == year)
|
||||||
|
if report_type is not None:
|
||||||
|
query = query.where(RfProjectReport.report_type == report_type)
|
||||||
|
if project_id is not None:
|
||||||
|
query = query.where(RfProjectReport.project_id == project_id)
|
||||||
|
|
||||||
|
return (await self.db.execute(query)).scalars().all()
|
||||||
|
|||||||
106
api/src/services/form3_phase_service.py
Normal file
106
api/src/services/form3_phase_service.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.form3_phase import Form3Phase
|
||||||
|
from src.db.models.form_type import FormType, FormTypeEnum
|
||||||
|
from src.repository.form3_phase_repository import Form3PhaseRepository
|
||||||
|
from src.core.errors import AccessDeniedException, ValidationsError
|
||||||
|
from src.db.models.role import UserRoleEnum
|
||||||
|
from src.db.models.app_user import AppUser
|
||||||
|
from src.domain.schemas import Form3PhaseCreate, Form3PhaseUpdate
|
||||||
|
|
||||||
|
|
||||||
|
class Form3PhaseService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
self.f3p_repo = Form3PhaseRepository(db)
|
||||||
|
|
||||||
|
async def get_list(
|
||||||
|
self,
|
||||||
|
report_id: int | list[int],
|
||||||
|
phase_code: str | None = None,
|
||||||
|
) -> list[Form3Phase]:
|
||||||
|
return await self.f3p_repo.get_list(
|
||||||
|
report_id=report_id,
|
||||||
|
phase_code=phase_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self, report_id: int, phase_code: str, user: AppUser
|
||||||
|
) -> Form3Phase | None:
|
||||||
|
return await self.f3p_repo.get(
|
||||||
|
report_id=report_id,
|
||||||
|
phase_code=phase_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _validate_form3_phase(
|
||||||
|
self,
|
||||||
|
form_phase: Form3PhaseCreate | Form3PhaseUpdate,
|
||||||
|
) -> None:
|
||||||
|
errors = {}
|
||||||
|
if form_phase.column_keys:
|
||||||
|
form_type = FormType(code=FormTypeEnum.FORM_3)
|
||||||
|
for column_key in form_phase.column_keys:
|
||||||
|
section = column_key.split(".")
|
||||||
|
if section[0] not in form_type.section_list:
|
||||||
|
if "column_keys" in errors:
|
||||||
|
errors["column_keys"].append(f"Колонка {column_key} некорректна")
|
||||||
|
else:
|
||||||
|
errors["column_keys"] = [f"Колонка {column_key} некорректна"]
|
||||||
|
if errors:
|
||||||
|
raise ValidationsError(errors=errors)
|
||||||
|
|
||||||
|
|
||||||
|
async def create(
|
||||||
|
self,
|
||||||
|
report_id: int,
|
||||||
|
body: Form3PhaseCreate,
|
||||||
|
user: AppUser,
|
||||||
|
) -> Form3Phase:
|
||||||
|
if user.role_id != UserRoleEnum.ADMIN:
|
||||||
|
raise AccessDeniedException
|
||||||
|
|
||||||
|
self._validate_form3_phase(
|
||||||
|
form_phase=body,
|
||||||
|
)
|
||||||
|
return await self.f3p_repo.create(
|
||||||
|
report_id=report_id,
|
||||||
|
phase_code=body.phase_code,
|
||||||
|
role=body.role,
|
||||||
|
column_keys=body.column_keys,
|
||||||
|
opens_at=body.opens_at,
|
||||||
|
closes_at=body.closes_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def update(
|
||||||
|
self,
|
||||||
|
report_id: int,
|
||||||
|
phase_code: str,
|
||||||
|
body: Form3PhaseUpdate,
|
||||||
|
user: AppUser,
|
||||||
|
) -> Form3Phase:
|
||||||
|
if user.role_id != UserRoleEnum.ADMIN:
|
||||||
|
raise AccessDeniedException
|
||||||
|
|
||||||
|
self._validate_form3_phase(
|
||||||
|
form_phase=body,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = body.model_dump(exclude_unset=True)
|
||||||
|
if not data:
|
||||||
|
return await self.f3p_repo.get(
|
||||||
|
report_id=report_id,
|
||||||
|
phase_code=phase_code,
|
||||||
|
)
|
||||||
|
return await self.f3p_repo.update(
|
||||||
|
report_id=report_id,
|
||||||
|
phase_code=phase_code,
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def delete(
|
||||||
|
self, report_id: int, phase_code: str, user: AppUser,
|
||||||
|
) -> bool:
|
||||||
|
return await self.f3p_repo.delete(
|
||||||
|
report_id=report_id,
|
||||||
|
phase_code=phase_code,
|
||||||
|
)
|
||||||
@ -1,5 +1,6 @@
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.project import Project
|
||||||
from src.db.models.app_user import AppUser
|
from src.db.models.app_user import AppUser
|
||||||
from src.db.models.role import UserRoleEnum
|
from src.db.models.role import UserRoleEnum
|
||||||
from src.core.errors import AccessDeniedException, ValidationException
|
from src.core.errors import AccessDeniedException, ValidationException
|
||||||
@ -41,6 +42,10 @@ class ProjectService:
|
|||||||
project["reports"] = await self.project_repo.get_reports(project_id=project_id)
|
project["reports"] = await self.project_repo.get_reports(project_id=project_id)
|
||||||
return project
|
return project
|
||||||
|
|
||||||
|
async def get_instance(self, project_id: int, user: AppUser) -> Project | None:
|
||||||
|
org_unit_ids = await self._allowed_org_unit_ids(user)
|
||||||
|
return await self.project_repo.get_instance(project_id=project_id, org_unit_ids=org_unit_ids)
|
||||||
|
|
||||||
async def resolve_report_id(
|
async def resolve_report_id(
|
||||||
self,
|
self,
|
||||||
project_id: int,
|
project_id: int,
|
||||||
|
|||||||
@ -3,8 +3,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
|
|
||||||
from src.db.models.rf_project_report import RfProjectReport
|
from src.db.models.rf_project_report import RfProjectReport
|
||||||
from src.repository.rf_project_report_repository import RfProjectReportRepository
|
from src.repository.rf_project_report_repository import RfProjectReportRepository
|
||||||
from src.db.models.rf_project_report_line import RfProjectReportLine
|
|
||||||
from src.repository.rf_project_report_line_repository import RfProjectReportLineRepository
|
|
||||||
from src.db.models.app_user import AppUser
|
from src.db.models.app_user import AppUser
|
||||||
|
|
||||||
|
|
||||||
@ -22,10 +20,26 @@ class RfProjectReportService:
|
|||||||
year: int | None = None,
|
year: int | None = None,
|
||||||
report_type: str | None = None,
|
report_type: str | None = None,
|
||||||
project_id: int | None = None,
|
project_id: int | None = None,
|
||||||
|
load_project: bool = False,
|
||||||
) -> RfProjectReport | None:
|
) -> RfProjectReport | None:
|
||||||
return await self.pr_repo.get(
|
return await self.pr_repo.get(
|
||||||
report_id=report_id,
|
report_id=report_id,
|
||||||
year=year,
|
year=year,
|
||||||
report_type=report_type,
|
report_type=report_type,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
|
load_project=load_project,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_list(
|
||||||
|
self,
|
||||||
|
report_id: int | None = None,
|
||||||
|
year: int | None = None,
|
||||||
|
report_type: str | None = None,
|
||||||
|
project_id: int | None = None,
|
||||||
|
) -> list[RfProjectReport]:
|
||||||
|
return await self.pr_repo.get_list(
|
||||||
|
report_id=report_id,
|
||||||
|
year=year,
|
||||||
|
report_type=report_type,
|
||||||
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -154,3 +154,165 @@ def test_form_phases_delete(client, admin_tokens):
|
|||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_form3_phases_list_smoke(client, admin_tokens):
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/stages/project/2", headers=_auth_headers(admin_tokens)
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert "result" in payload
|
||||||
|
assert "count" in payload
|
||||||
|
assert isinstance(payload["result"], list)
|
||||||
|
|
||||||
|
|
||||||
|
def test_form3_phases_list_filter_smoke(client, admin_tokens):
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/stages/project/2", headers=_auth_headers(admin_tokens)
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert "count" in payload
|
||||||
|
count = payload["count"]
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/stages/project/2?&phase_code=test", 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(
|
||||||
|
"/api/v1/stages/project/2",
|
||||||
|
json={
|
||||||
|
"year": 2026,
|
||||||
|
"report_type": "LIMIT",
|
||||||
|
"phase_code": "CODE1",
|
||||||
|
"role": "DFIP",
|
||||||
|
"column_keys": [
|
||||||
|
"q1.adj_by_items",
|
||||||
|
"q1.adj_increase"
|
||||||
|
],
|
||||||
|
"opens_at": "2027-07-20T00:00:00",
|
||||||
|
"closes_at": "2027-09-21T00:00:00"
|
||||||
|
},
|
||||||
|
headers=_auth_headers(isp_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
payload = response.json()
|
||||||
|
assert isinstance(payload, dict)
|
||||||
|
assert "message" in payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_form3_phases_update_forbidden(client, isp_tokens):
|
||||||
|
response = client.patch(
|
||||||
|
"/api/v1/stages/project/2/2026/LIMIT/RF_FILL_2026",
|
||||||
|
json={
|
||||||
|
"role": "DFIP",
|
||||||
|
"column_keys": [
|
||||||
|
"q1.adj_by_items",
|
||||||
|
"q1.adj_increase"
|
||||||
|
],
|
||||||
|
"opens_at": "2027-07-20T00:00:00",
|
||||||
|
"closes_at": "2027-09-21T00:00:00"
|
||||||
|
},
|
||||||
|
headers=_auth_headers(isp_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
payload = response.json()
|
||||||
|
assert isinstance(payload, dict)
|
||||||
|
assert "message" in payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_form3_phases_delete_forbidden(client, isp_tokens):
|
||||||
|
response = client.delete(
|
||||||
|
"/api/v1/stages/project/2/2026/LIMIT/RF_FILL_2026",
|
||||||
|
headers=_auth_headers(isp_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
payload = response.json()
|
||||||
|
assert isinstance(payload, dict)
|
||||||
|
assert "message" in payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_form3_phases_create(client, admin_tokens):
|
||||||
|
body = {
|
||||||
|
"year": 2026,
|
||||||
|
"report_type": "LIMIT",
|
||||||
|
"phase_code": "CODE1",
|
||||||
|
"role": "DFIP",
|
||||||
|
"column_keys": [
|
||||||
|
"q1.adj_by_items",
|
||||||
|
"q1.adj_increase"
|
||||||
|
],
|
||||||
|
"opens_at": "2027-07-20T00:00:00",
|
||||||
|
"closes_at": "2027-09-21T00:00:00"
|
||||||
|
}
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/stages/project/2",
|
||||||
|
json=body,
|
||||||
|
headers=_auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert "success" in payload
|
||||||
|
assert payload["success"]
|
||||||
|
assert "result" in payload
|
||||||
|
result = payload["result"]
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
opens_at = result.pop("opens_at", None)
|
||||||
|
assert "+03" in opens_at
|
||||||
|
assert body.pop("opens_at") == opens_at.split("+")[0]
|
||||||
|
closes_at = result.pop("closes_at", None)
|
||||||
|
assert "+03" in closes_at
|
||||||
|
assert body.pop("closes_at") == closes_at.split("+")[0]
|
||||||
|
assert result == body
|
||||||
|
|
||||||
|
assert payload["result"] == body
|
||||||
|
|
||||||
|
|
||||||
|
def test_form3_phases_update(client, admin_tokens):
|
||||||
|
body = {
|
||||||
|
"role": "DFIP",
|
||||||
|
"column_keys": [
|
||||||
|
"q1.adj_by_items",
|
||||||
|
"q1.adj_increase"
|
||||||
|
],
|
||||||
|
"opens_at": "2027-07-20T00:00:00",
|
||||||
|
"closes_at": "2027-09-21T00:00:00"
|
||||||
|
}
|
||||||
|
response = client.patch(
|
||||||
|
"/api/v1/stages/project/2/2026/LIMIT/RF_FILL_2026",
|
||||||
|
json=body,
|
||||||
|
headers=_auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
|
||||||
|
assert "success" in payload
|
||||||
|
assert payload["success"]
|
||||||
|
assert "result" in payload
|
||||||
|
result = payload["result"]
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
opens_at = result.pop("opens_at", None)
|
||||||
|
assert "+03" in opens_at
|
||||||
|
assert body.pop("opens_at") == opens_at.split("+")[0]
|
||||||
|
closes_at = result.pop("closes_at", None)
|
||||||
|
assert "+03" in closes_at
|
||||||
|
assert body.pop("closes_at") == closes_at.split("+")[0]
|
||||||
|
|
||||||
|
body.update({'phase_code': 'RF_FILL_2026', 'report_type': 'LIMIT', 'year': 2026})
|
||||||
|
assert result == body
|
||||||
|
|
||||||
|
|
||||||
|
def test_form3_phases_delete(client, admin_tokens):
|
||||||
|
response = client.delete(
|
||||||
|
"/api/v1/stages/project/2/2026/LIMIT/RF_FILL_2026",
|
||||||
|
headers=_auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|||||||
@ -119,6 +119,49 @@ def test_org_units_update_offset_smoke(client, admin_tokens, auth_headers):
|
|||||||
assert phase_old["closes_at"].split(old_offset)[0] == phase_new["closes_at"].split(new_offset)[0]
|
assert phase_old["closes_at"].split(old_offset)[0] == phase_new["closes_at"].split(new_offset)[0]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_org_units_form3_update_offset_smoke(client, admin_tokens, auth_headers):
|
||||||
|
old_offset = "+03"
|
||||||
|
new_offset = "+04"
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/stages/project/2", headers=auth_headers(admin_tokens)
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert "result" in payload
|
||||||
|
phases = payload["result"]
|
||||||
|
assert isinstance(phases, list)
|
||||||
|
assert len(phases)
|
||||||
|
phase_old = phases[0]
|
||||||
|
|
||||||
|
response = client.patch(
|
||||||
|
"/api/v1/org-unit/1",
|
||||||
|
json={"utc_offset": new_offset},
|
||||||
|
headers=auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert payload["result"]["id"] == 1
|
||||||
|
assert payload["result"]["utc_offset"] == new_offset
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/stages/project/2", headers=auth_headers(admin_tokens)
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert "result" in payload
|
||||||
|
phases = payload["result"]
|
||||||
|
assert isinstance(phases, list)
|
||||||
|
assert len(phases)
|
||||||
|
phase_new = phases[0]
|
||||||
|
|
||||||
|
assert new_offset in phase_new["opens_at"]
|
||||||
|
assert new_offset in phase_new["closes_at"]
|
||||||
|
print( phase_old, phase_new)
|
||||||
|
assert phase_old["opens_at"].split(old_offset)[0] == phase_new["opens_at"].split(new_offset)[0]
|
||||||
|
assert phase_old["closes_at"].split(old_offset)[0] == phase_new["closes_at"].split(new_offset)[0]
|
||||||
|
|
||||||
|
|
||||||
def test_org_units_update_not_found_error_shape(client, admin_tokens, auth_headers):
|
def test_org_units_update_not_found_error_shape(client, admin_tokens, auth_headers):
|
||||||
response = client.patch(
|
response = client.patch(
|
||||||
"/api/v1/org-unit/99999",
|
"/api/v1/org-unit/99999",
|
||||||
|
|||||||
@ -32,3 +32,16 @@ INSERT INTO v3.vsp (id,branch_id,reg_number,address,format,opened_at,placement_t
|
|||||||
(2,2,'Test12 -----','Тестовая 125','формат 15 ','2026-06-08',NULL,NULL,58,NULL,true,'2026-06-16 18:53:17.986651',false,'2026-06-09 09:04:33.29858',91,'123-1234',91,'субаренда','Работает ','12-45','2027-10-02'),
|
(2,2,'Test12 -----','Тестовая 125','формат 15 ','2026-06-08',NULL,NULL,58,NULL,true,'2026-06-16 18:53:17.986651',false,'2026-06-09 09:04:33.29858',91,'123-1234',91,'субаренда','Работает ','12-45','2027-10-02'),
|
||||||
(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',91,'3434',NULL,'2','примечание','567','2027-07-01');
|
(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',91,'3434',NULL,'2','примечание','567','2027-07-01');
|
||||||
SELECT setval('v3.vsp_id_seq', 3);
|
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);
|
||||||
|
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');
|
||||||
|
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
|
||||||
|
(1,'RF_FILL_2026','EXECUTOR_RF','{{q1.adj_by_items,q1.adj_increase,q1.m1,q1.m2,q1.m3,q2.adj_by_items,q2.adj_increase,q2.m1,q2.m2,q2.m3,q3.adj_by_items,q3.adj_increase,q3.m1,q3.m2,q3.m3,q4.adj_by_items,q4.adj_increase,q4.m1,q4.m2,q4.m3,q4.spod}}','2030-01-01 03:00:00+03','2031-01-01 02:59:59+03'),
|
||||||
|
(2,'RF_FILL_2026','EXECUTOR_RF','{{q1.adj_by_items,q1.adj_increase,q1.m1,q1.m2,q1.m3,q2.adj_by_items,q2.adj_increase,q2.m1,q2.m2,q2.m3,q3.adj_by_items,q3.adj_increase,q3.m1,q3.m2,q3.m3,q4.adj_by_items,q4.adj_increase,q4.m1,q4.m2,q4.m3,q4.spod}}','2030-01-01 03:00:00+03','2031-01-01 02:59:59+03');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user