diff --git a/api/alembic/versions/0020_carryover_form3.py b/api/alembic/versions/0020_carryover_form3.py new file mode 100644 index 0000000..c966885 --- /dev/null +++ b/api/alembic/versions/0020_carryover_form3.py @@ -0,0 +1,82 @@ +import os +import re + +from alembic import op + + +revision = "0020" +down_revision = "0019" +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", "0020_carryover_form3.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 diff --git a/api/alembic/versions/sql/0020_carryover_form3.sql b/api/alembic/versions/sql/0020_carryover_form3.sql new file mode 100644 index 0000000..3cd6d7c --- /dev/null +++ b/api/alembic/versions/sql/0020_carryover_form3.sql @@ -0,0 +1,353 @@ +ALTER TABLE v3.rf_project_report_quarter ADD COLUMN carryover NUMERIC; + +-- DROP FUNCTION v3.v_form3_report_sections(int4, _text); + +CREATE OR REPLACE FUNCTION v3.v_form3_report_sections(p_report_id integer, p_sections text[] DEFAULT NULL::text[]) + RETURNS TABLE(row_type character varying, depth integer, line_id 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_carryover 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_carryover 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_carryover 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, col_q1_base_plan numeric, col_q2_base_plan numeric, col_q3_base_plan numeric, col_q4_base_plan numeric, col_year_base_plan numeric, col_q2_corrected_plan numeric, col_q3_corrected_plan numeric, col_q4_corrected_plan 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 + IF NOT EXISTS (SELECT 1 FROM v3.rf_project_report WHERE id = p_report_id) THEN + RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id; + END IF; + + RETURN QUERY + WITH + -- Дерево expense_item, ограниченное FORM_3 (через junction) + 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' + ), + -- INPUT строки: одна на v3.rf_project_report_line + page AS ( + SELECT l.id AS lid, l.expense_item_id AS eid, + t.parent_item_id AS sc, -- section-код родителя в дереве (1.01.1.) + ei.item_id AS ic, + ei.num_group_id AS ng, ei.name AS ename, + t.path AS tree_path + FROM v3.rf_project_report_line l + JOIN v3.expense_item ei ON ei.id = l.expense_item_id + JOIN tw t ON t.id = l.expense_item_id + WHERE l.rf_project_report_id = p_report_id + ), + -- Поквартальные данные + jq AS ( + SELECT q.rf_project_report_line_id AS lid, q.quarter, + q.adj_by_items, q.adj_increase, + q.actual_m1, q.actual_m2, q.actual_m3, q.actual_spod, + q.base_plan, q.corrected_plan, q.carryover + FROM v3.rf_project_report_quarter q + JOIN v3.rf_project_report_line l ON l.id = q.rf_project_report_line_id + WHERE l.rf_project_report_id = p_report_id + ), + -- INPUT base: соединяем page + 4 квартала + input_base AS ( + SELECT + pg.lid, pg.eid, pg.sc, pg.ic, pg.ng, pg.ename, pg.tree_path, + q1.adj_by_items AS q1_abi, q1.adj_increase AS q1_ai, + q1.actual_m1 AS q1_m1, q1.actual_m2 AS q1_m2, q1.actual_m3 AS q1_m3, + q2.carryover as q2_co, + q2.adj_by_items AS q2_abi, q2.adj_increase AS q2_ai, + q2.actual_m1 AS q2_m1, q2.actual_m2 AS q2_m2, q2.actual_m3 AS q2_m3, + q3.carryover as q3_co, + q3.adj_by_items AS q3_abi, q3.adj_increase AS q3_ai, + q3.actual_m1 AS q3_m1, q3.actual_m2 AS q3_m2, q3.actual_m3 AS q3_m3, + q4.carryover as q4_co, + q4.adj_by_items AS q4_abi, q4.adj_increase AS q4_ai, + q4.actual_m1 AS q4_m1, q4.actual_m2 AS q4_m2, q4.actual_m3 AS q4_m3, + q4.actual_spod AS q4_spod, + q1.base_plan AS q1_bp, q2.base_plan AS q2_bp, + q3.base_plan AS q3_bp, q4.base_plan AS q4_bp, + q2.corrected_plan AS q2_cp, q3.corrected_plan AS q3_cp, q4.corrected_plan AS q4_cp + FROM page pg + LEFT JOIN jq q1 ON q1.lid = pg.lid AND q1.quarter = 1 + LEFT JOIN jq q2 ON q2.lid = pg.lid AND q2.quarter = 2 + LEFT JOIN jq q3 ON q3.lid = pg.lid AND q3.quarter = 3 + LEFT JOIN jq q4 ON q4.lid = pg.lid AND q4.quarter = 4 + ), + -- Computed totals per row + enriched AS ( + SELECT b.*, + COALESCE(b.q1_abi,0) + COALESCE(b.q1_ai,0) AS q1_tc, + COALESCE(b.q1_m1,0) + COALESCE(b.q1_m2,0) + COALESCE(b.q1_m3,0) AS q1_qa, + COALESCE(b.q2_abi,0) + COALESCE(b.q2_ai,0) AS q2_tc, + COALESCE(b.q2_m1,0) + COALESCE(b.q2_m2,0) + COALESCE(b.q2_m3,0) AS q2_qa, + COALESCE(b.q3_abi,0) + COALESCE(b.q3_ai,0) AS q3_tc, + COALESCE(b.q3_m1,0) + COALESCE(b.q3_m2,0) + COALESCE(b.q3_m3,0) AS q3_qa, + COALESCE(b.q4_abi,0) + COALESCE(b.q4_ai,0) AS q4_tc, + COALESCE(b.q4_m1,0) + COALESCE(b.q4_m2,0) + COALESCE(b.q4_m3,0) + COALESCE(b.q4_spod,0) AS q4_qa + FROM input_base b + ), + -- Per-line aggregates for tree rollup + 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, + SUM(q.base_plan) AS bp, SUM(q.corrected_plan) AS cp, SUM(q.carryover) as co + FROM v3.rf_project_report_line l + JOIN v3.rf_project_report_quarter q ON q.rf_project_report_line_id = l.id + WHERE l.rf_project_report_id = p_report_id + 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, + SUM(a.bp) AS bp + 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, + SUM(a.bp) AS bp, SUM(a.cp) AS cp, SUM(a.co) AS co + 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, + SUM(a.bp) AS bp, SUM(a.cp) AS cp, SUM(a.co) AS co + 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, + SUM(a.bp) AS bp, SUM(a.cp) AS cp, SUM(a.co) AS co + FROM tw LEFT JOIN agg_q a ON a.eid = ANY(tw.desc_ids) AND a.quarter = 4 + GROUP BY tw.id + ) + + -- ═══ Часть A: INPUT строки ════════════════════════════════════════════════ + SELECT * FROM ( + SELECT + 'INPUT'::VARCHAR AS row_type, 3 AS depth, e.lid::INT AS line_id, + e.sc, e.ic, e.ng, e.ename, + -- Q1 + CASE WHEN s_q1 THEN e.q1_abi END, + CASE WHEN s_q1 THEN e.q1_ai END, + CASE WHEN s_q1 THEN e.q1_tc END, + CASE WHEN s_q1 THEN e.q1_m1 END, + CASE WHEN s_q1 THEN e.q1_m2 END, + CASE WHEN s_q1 THEN e.q1_m3 END, + CASE WHEN s_q1 THEN e.q1_qa END, + CASE WHEN s_q1 THEN ROUND(e.q1_tc - e.q1_qa, 1) END, + -- Q2 (carryover пока NULL — нет столбца в _quarter; задел на будущее) + CASE WHEN s_q2 THEN e.q2_co END, + CASE WHEN s_q2 THEN e.q2_abi END, + CASE WHEN s_q2 THEN e.q2_ai END, + CASE WHEN s_q2 THEN e.q2_tc END, + CASE WHEN s_q2 THEN e.q2_m1 END, + CASE WHEN s_q2 THEN e.q2_m2 END, + CASE WHEN s_q2 THEN e.q2_m3 END, + CASE WHEN s_q2 THEN e.q2_qa END, + CASE WHEN s_q2 THEN ROUND(e.q2_tc - e.q2_qa, 1) END, + -- Q3 + CASE WHEN s_q3 THEN e.q3_co END, + CASE WHEN s_q3 THEN e.q3_abi END, + CASE WHEN s_q3 THEN e.q3_ai END, + CASE WHEN s_q3 THEN e.q3_tc END, + CASE WHEN s_q3 THEN e.q3_m1 END, + CASE WHEN s_q3 THEN e.q3_m2 END, + CASE WHEN s_q3 THEN e.q3_m3 END, + CASE WHEN s_q3 THEN e.q3_qa END, + CASE WHEN s_q3 THEN ROUND(e.q3_tc - e.q3_qa, 1) END, + -- Q4 + CASE WHEN s_q4 THEN e.q4_co END, + CASE WHEN s_q4 THEN e.q4_abi END, + CASE WHEN s_q4 THEN e.q4_ai END, + CASE WHEN s_q4 THEN e.q4_tc END, + CASE WHEN s_q4 THEN e.q4_m1 END, + CASE WHEN s_q4 THEN e.q4_m2 END, + CASE WHEN s_q4 THEN e.q4_m3 END, + CASE WHEN s_q4 THEN e.q4_spod END, + CASE WHEN s_q4 THEN e.q4_qa END, + CASE WHEN s_q4 THEN ROUND(e.q4_tc - e.q4_qa, 1) END, + -- year + CASE WHEN s_year THEN e.q1_tc + e.q2_tc + e.q3_tc + e.q4_tc END, + CASE WHEN s_year THEN e.q1_qa + e.q2_qa + e.q3_qa + e.q4_qa END, + CASE WHEN s_year THEN ROUND((e.q1_tc + e.q2_tc + e.q3_tc + e.q4_tc) - + (e.q1_qa + e.q2_qa + e.q3_qa + e.q4_qa), 1) END, + -- план-блоки + CASE WHEN s_q1 THEN e.q1_bp END, + CASE WHEN s_q2 THEN e.q2_bp END, + CASE WHEN s_q3 THEN e.q3_bp END, + CASE WHEN s_q4 THEN e.q4_bp END, + CASE WHEN s_year THEN COALESCE(e.q1_bp,0)+COALESCE(e.q2_bp,0)+ + COALESCE(e.q3_bp,0)+COALESCE(e.q4_bp,0) END, + CASE WHEN s_q2 THEN e.q2_cp END, + CASE WHEN s_q3 THEN e.q3_cp END, + CASE WHEN s_q4 THEN e.q4_cp END, + e.tree_path || ARRAY[e.lid::INT] AS _sort_path + FROM enriched e + + UNION ALL + + -- ═══ Часть B: Иерархия (ROOT / GROUP / ITEM / SUB_ITEM) ═════════════════ + 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, NULL::INT, + 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.co END, + 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.co END, + 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.co END, + 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, + -- план-блоки (rollup) + CASE WHEN s_q1 THEN a1.bp END, + CASE WHEN s_q2 THEN a2.bp END, + CASE WHEN s_q3 THEN a3.bp END, + CASE WHEN s_q4 THEN a4.bp END, + CASE WHEN s_year THEN COALESCE(a1.bp,0)+COALESCE(a2.bp,0)+ + COALESCE(a3.bp,0)+COALESCE(a4.bp,0) END, + CASE WHEN s_q2 THEN a2.cp END, + CASE WHEN s_q3 THEN a3.cp END, + CASE WHEN s_q4 THEN a4.cp 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 + ) sub + ORDER BY sub._sort_path; + +END; +$function$ +; + + + +-- DROP FUNCTION v3._apply_form3_cell(int4, text, jsonb); + +CREATE OR REPLACE FUNCTION v3._apply_form3_cell(p_line_id integer, p_column text, p_value jsonb) + RETURNS void + LANGUAGE plpgsql +AS $function$ +DECLARE + v_parts TEXT[]; + v_scope TEXT; + v_field TEXT; + v_q SMALLINT; + v_target_col TEXT; + v_target_type TEXT; + v_sql TEXT; +BEGIN + v_parts := string_to_array(p_column, '.'); + IF array_length(v_parts, 1) < 2 THEN + RAISE EXCEPTION 'bad_column_format: %, expected scope.field', p_column; + END IF; + v_scope := v_parts[1]; + v_field := v_parts[2]; + + IF v_scope = 'totals' OR v_field IN ('total_corr','quarter_actual','economy') THEN + RAISE EXCEPTION 'computed_field: %', p_column; + END IF; + + IF v_scope NOT IN ('q1','q2','q3','q4') THEN + RAISE EXCEPTION 'unknown_scope: % (FORM_3 supports only q1..q4)', v_scope; + END IF; + + v_q := substring(v_scope FROM 2)::SMALLINT; + + -- Маппинг JSONB-ключ → колонка rf_project_report_quarter + CASE v_field + WHEN 'adj_by_items' THEN v_target_col := 'adj_by_items'; v_target_type := 'NUMERIC'; + WHEN 'carryover' THEN v_target_col := 'carryover'; v_target_type := 'NUMERIC'; + WHEN 'adj_increase' THEN v_target_col := 'adj_increase'; v_target_type := 'NUMERIC'; + WHEN 'm1' THEN v_target_col := 'actual_m1'; v_target_type := 'NUMERIC'; + WHEN 'm2' THEN v_target_col := 'actual_m2'; v_target_type := 'NUMERIC'; + WHEN 'm3' THEN v_target_col := 'actual_m3'; v_target_type := 'NUMERIC'; + WHEN 'spod' THEN + IF v_q <> 4 THEN RAISE EXCEPTION 'spod is q4-only: %', p_column; END IF; + v_target_col := 'actual_spod'; v_target_type := 'NUMERIC'; + WHEN 'base_plan' THEN v_target_col := 'base_plan'; v_target_type := 'NUMERIC'; + WHEN 'corrected_plan' THEN + IF v_q = 1 THEN RAISE EXCEPTION 'corrected_plan is q2-q4 only: %', p_column; END IF; + v_target_col := 'corrected_plan'; v_target_type := 'NUMERIC'; + ELSE + RAISE EXCEPTION 'unknown_column: %', p_column; + END CASE; + + v_sql := format( + 'INSERT INTO v3.rf_project_report_quarter (rf_project_report_line_id, quarter, %1$I) ' + || 'VALUES ($1, $2, ($3 #>> ''{}'')::%2$s) ' + || 'ON CONFLICT (rf_project_report_line_id, quarter) DO UPDATE SET %1$I = EXCLUDED.%1$I', + v_target_col, v_target_type + ); + EXECUTE v_sql USING p_line_id, v_q, p_value; +END; +$function$ +; \ No newline at end of file diff --git a/api/src/db/models/rf_project_report_quarter.py b/api/src/db/models/rf_project_report_quarter.py index ed6560b..4378aae 100644 --- a/api/src/db/models/rf_project_report_quarter.py +++ b/api/src/db/models/rf_project_report_quarter.py @@ -24,6 +24,7 @@ class RfProjectReportQuarter(Base): actual_m2: Mapped[float | None] = mapped_column(Numeric) actual_m3: Mapped[float | None] = mapped_column(Numeric) actual_spod: Mapped[float | None] = mapped_column(Numeric) + carryover: Mapped[float | None] = mapped_column(Numeric) # line: Mapped[RfProjectReportLine] = relationship( # "RfProjectReportLine", back_populates="quarters"