Compare commits
2 Commits
15477cf6ce
...
efe6df992d
| Author | SHA1 | Date | |
|---|---|---|---|
| efe6df992d | |||
| 68fd2a7646 |
84
api/alembic/versions/0010_form4.py
Normal file
84
api/alembic/versions/0010_form4.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision = "0010"
|
||||||
|
down_revision = "0009"
|
||||||
|
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", "0010_form4.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
|
||||||
852
api/alembic/versions/sql/0010_form4.sql
Normal file
852
api/alembic/versions/sql/0010_form4.sql
Normal file
@ -0,0 +1,852 @@
|
|||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- migrate_form4_project.sql — выделение FORM_4 программ/проектов в отдельную
|
||||||
|
-- сущность v3.form4_project (перестают делить таблицу с FORM_3 v3.project).
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Мотивация: v3.project исторически был FORM_4-таблицей (program/project +
|
||||||
|
-- parent_id), FORM_3-редизайн навесил сверху свои атрибуты (project_type,
|
||||||
|
-- vsp_format, address, area, staff, org_unit_id) и строгий chk_v3_project_name.
|
||||||
|
-- Два разных концепта под одним словом «project» → разреженные колонки, нет
|
||||||
|
-- защиты FK, констрейнт вынужден ветвиться по «виду» строки. Разводим.
|
||||||
|
--
|
||||||
|
-- Новая таблица v3.form4_project — точный костяк без FORM_3-полей:
|
||||||
|
-- program (level='program', parent_id=NULL)
|
||||||
|
-- project (level='project', parent_id=program)
|
||||||
|
--
|
||||||
|
-- Дискриминатор FORM_4 vs FORM_3: FORM_3-строки — те, что ссылаются из
|
||||||
|
-- v3.rf_project_report (add_project всегда создаёт отчёты для КАЖДОЙ строки,
|
||||||
|
-- включая программы). Всё остальное в v3.project — FORM_4.
|
||||||
|
--
|
||||||
|
-- Порядок: после init.sql + всех migrate_form3*/migrate_to_identity (нужна
|
||||||
|
-- финальная форма v3.project с org_unit_id). Один прогон — copy защищён
|
||||||
|
-- проверкой на пустоту form4_project.
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- ───── 1. Новая таблица ─────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS v3.form4_project (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR NOT NULL,
|
||||||
|
level VARCHAR CHECK (level IN ('program', 'project')),
|
||||||
|
section_code VARCHAR,
|
||||||
|
parent_id INTEGER REFERENCES v3.form4_project(id),
|
||||||
|
-- org_unit_id INTEGER REFERENCES v3.org_unit(id),
|
||||||
|
form_id INTEGER REFERENCES v3.budget_form(id),
|
||||||
|
-- project (level='project') обязан иметь родителя-программу; program — нет.
|
||||||
|
CONSTRAINT chk_form4_project_parent CHECK (
|
||||||
|
(level = 'program' AND parent_id IS NULL)
|
||||||
|
OR (level = 'project')
|
||||||
|
),
|
||||||
|
CONSTRAINT chk_form4_project_section_code CHECK (
|
||||||
|
(level = 'program') = (section_code IS NOT NULL)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX ix_form4_project_section_code
|
||||||
|
ON v3.form4_project(section_code) WHERE level = 'program';
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_v3_form4_project_parent ON v3.form4_project(parent_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_v3_form4_project_form ON v3.form4_project(form_id);
|
||||||
|
|
||||||
|
-- ───── 2. Перенос FORM_4-строк из v3.project (с сохранением id) ──────────────
|
||||||
|
-- FK budget_line.project_id ссылается по id → id сохраняем 1:1.
|
||||||
|
-- Копируем обе роли (program + project) одним INSERT: self-FK parent_id внутри
|
||||||
|
-- одного оператора проверяется в конце, порядок строк не важен.
|
||||||
|
-- DO $migrate$
|
||||||
|
-- BEGIN
|
||||||
|
-- IF NOT EXISTS (SELECT 1 FROM v3.form4_project) THEN
|
||||||
|
-- INSERT INTO v3.form4_project (id, name, level, parent_id, org_unit_id)
|
||||||
|
-- SELECT p.id, p.name, p.level, p.parent_id, (p.org_unit_id
|
||||||
|
-- FROM v3.project p
|
||||||
|
-- WHERE p.id NOT IN (
|
||||||
|
-- SELECT project_id FROM v3.rf_project_report WHERE project_id IS NOT NULL
|
||||||
|
-- );
|
||||||
|
|
||||||
|
-- -- Синхронизируем sequence, чтобы новые INSERT не конфликтовали по id.
|
||||||
|
-- PERFORM setval('v3.form4_project_id_seq',
|
||||||
|
-- GREATEST((SELECT COALESCE(MAX(id), 0) FROM v3.form4_project), 1));
|
||||||
|
|
||||||
|
-- RAISE NOTICE 'form4_project: перенесено % строк из v3.project',
|
||||||
|
-- (SELECT count(*) FROM v3.form4_project);
|
||||||
|
-- ELSE
|
||||||
|
-- RAISE NOTICE 'form4_project уже наполнена — перенос пропущен';
|
||||||
|
-- END IF;
|
||||||
|
-- END
|
||||||
|
-- $migrate$;
|
||||||
|
|
||||||
|
-- ───── 3. Перецепка FK budget_line.project_id на новую таблицу ──────────────
|
||||||
|
ALTER TABLE v3.budget_line DROP CONSTRAINT IF EXISTS budget_line_project_id_fkey;
|
||||||
|
ALTER TABLE v3.budget_line ADD CONSTRAINT budget_line_project_id_fkey FOREIGN KEY (project_id) REFERENCES v3.form4_project(id);
|
||||||
|
|
||||||
|
-- ───── 4. Удаление перенесённых строк из v3.project ─────────────────────────
|
||||||
|
-- После перецепки budget_line на v3.project больше не ссылается ничего FORM_4.
|
||||||
|
-- Остаются только FORM_3-строки (referenced by rf_project_report).
|
||||||
|
DELETE FROM v3.project p
|
||||||
|
WHERE p.id NOT IN (
|
||||||
|
SELECT project_id FROM v3.rf_project_report WHERE project_id IS NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ───── 5. Восстановление chk_v3_project_name в строгой форме ─────────────────
|
||||||
|
-- FORM_4-строки ушли — v3.project теперь чисто FORM_3, правило коротких имён
|
||||||
|
-- ВСП применимо ко всей таблице (как до form4_write.sql). NOT VALID сохраняем.
|
||||||
|
ALTER TABLE v3.project DROP CONSTRAINT IF EXISTS chk_v3_project_name;
|
||||||
|
ALTER TABLE v3.project
|
||||||
|
ADD CONSTRAINT chk_v3_project_name
|
||||||
|
CHECK (name IS NULL OR (
|
||||||
|
length(name) <= 30 AND
|
||||||
|
name !~ E'[+\\-/\\\\=&* ]'
|
||||||
|
)) NOT VALID;
|
||||||
|
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- v3.add_form4_program / v3.add_form4_project — создание программ и проектов
|
||||||
|
-- FORM_4 в выделенной таблице v3.form4_project (иерархия program → project).
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- FORM_4 хранит иерархию отдельно от FORM_3 (v3.project) — см.
|
||||||
|
-- migrate_form4_project.sql. Костяк:
|
||||||
|
-- program (level='program', parent_id=NULL) → depth=1 (GROUP) в v_form4
|
||||||
|
-- project (level='project', parent_id=program) → depth=2 (ITEM) в v_form4
|
||||||
|
-- Строки бюджета привязываются к проекту через budget_line.project_id.
|
||||||
|
--
|
||||||
|
-- Никаких FORM_3-атрибутов / rf_project_report / snapshot фаз — это ветка
|
||||||
|
-- budget_form. p_org_unit_id опционален (read-path FORM_4 по нему не фильтрует,
|
||||||
|
-- заполняется для аудита/принадлежности).
|
||||||
|
--
|
||||||
|
-- Порядок применения: после migrate_form4_project.sql (нужна таблица
|
||||||
|
-- v3.form4_project) и audit.sql (нужна v3.log_event).
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- ───── 1. add_form4_program ─────────────────────────────────────────────────
|
||||||
|
DROP FUNCTION IF EXISTS v3.add_form4_program(VARCHAR, INT, VARCHAR, INT);
|
||||||
|
|
||||||
|
CREATE FUNCTION v3.add_form4_program(
|
||||||
|
p_name VARCHAR,
|
||||||
|
p_form_id INT,
|
||||||
|
p_section_code VARCHAR DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
project_id INT,
|
||||||
|
row_type VARCHAR,
|
||||||
|
depth INT,
|
||||||
|
sort_order BIGINT,
|
||||||
|
data JSONB
|
||||||
|
) AS $function$
|
||||||
|
#variable_conflict use_column
|
||||||
|
DECLARE
|
||||||
|
v_pid INT;
|
||||||
|
BEGIN
|
||||||
|
IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
|
||||||
|
RAISE EXCEPTION 'p_name обязателен';
|
||||||
|
END IF;
|
||||||
|
IF p_form_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_form_id обязателен';
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM v3.budget_form bf
|
||||||
|
WHERE bf.form_type_code = 'FORM_4' AND bf.id = p_form_id
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION 'Только FORM_4';
|
||||||
|
END IF;
|
||||||
|
IF p_section_code IS NOT NULL AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM v3.expense_item
|
||||||
|
WHERE section_code = p_section_code AND depth = 0
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION 'section_code=% не существует в expense_item (depth=0)', p_section_code;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
INSERT INTO v3.form4_project (name, level, parent_id, form_id, section_code)
|
||||||
|
VALUES (p_name, 'program', NULL, p_form_id, p_section_code)
|
||||||
|
RETURNING id INTO v_pid;
|
||||||
|
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'PROJECT_CREATE', 'PROJECT',
|
||||||
|
jsonb_strip_nulls(jsonb_build_object(
|
||||||
|
'project_id', v_pid,
|
||||||
|
'name', p_name,
|
||||||
|
'level', 'program',
|
||||||
|
'form_id', p_form_id,
|
||||||
|
'form_type', 'FORM_4'
|
||||||
|
)),
|
||||||
|
p_form_id, NULL, NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT v_pid, v.row_type, v.depth, v.sort_order, v.data
|
||||||
|
FROM v3.v_form4_sheet_jsonb(p_form_id, p_section_code, NULL) v
|
||||||
|
WHERE (v.row_type = 'GROUP' AND (v.data->'header'->>'program_id')::INT = v_pid)
|
||||||
|
OR (v.row_type = 'ROOT' AND v.data->'header'->>'section_code' = p_section_code)
|
||||||
|
ORDER BY v.sort_order;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql VOLATILE;
|
||||||
|
|
||||||
|
-- ───── 2. add_form4_project ─────────────────────────────────────────────────
|
||||||
|
DROP FUNCTION IF EXISTS v3.add_form4_project(VARCHAR, INT);
|
||||||
|
|
||||||
|
CREATE FUNCTION v3.add_form4_project(
|
||||||
|
p_name VARCHAR,
|
||||||
|
p_program_id INT
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
project_id INT,
|
||||||
|
row_type VARCHAR,
|
||||||
|
depth INT,
|
||||||
|
sort_order BIGINT,
|
||||||
|
data JSONB
|
||||||
|
)
|
||||||
|
AS $function$
|
||||||
|
#variable_conflict use_column
|
||||||
|
DECLARE
|
||||||
|
v_pid INT;
|
||||||
|
v_section_code VARCHAR;
|
||||||
|
v_form_id INT;
|
||||||
|
BEGIN
|
||||||
|
IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
|
||||||
|
RAISE EXCEPTION 'p_name обязателен';
|
||||||
|
END IF;
|
||||||
|
IF p_program_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_program_id обязателен (проект создаётся под программой)';
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.form4_project fp
|
||||||
|
WHERE fp.id = p_program_id AND fp.level = 'program') THEN
|
||||||
|
RAISE EXCEPTION 'program id=% не существует или не имеет level=program', p_program_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT form_id INTO v_form_id FROM v3.form4_project WHERE id = p_program_id;
|
||||||
|
|
||||||
|
INSERT INTO v3.form4_project (name, level, parent_id, form_id)
|
||||||
|
VALUES (p_name, 'project', p_program_id, v_form_id)
|
||||||
|
RETURNING id INTO v_pid;
|
||||||
|
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'PROJECT_CREATE', 'PROJECT',
|
||||||
|
jsonb_strip_nulls(jsonb_build_object(
|
||||||
|
'project_id', v_pid,
|
||||||
|
'name', p_name,
|
||||||
|
'level', 'project',
|
||||||
|
'parent_id', p_program_id,
|
||||||
|
'form_id', v_form_id,
|
||||||
|
'form_type', 'FORM_4'
|
||||||
|
)),
|
||||||
|
v_form_id, NULL, NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT fp.section_code INTO v_section_code
|
||||||
|
FROM v3.form4_project fp
|
||||||
|
WHERE fp.id = p_program_id AND fp.level = 'program';
|
||||||
|
|
||||||
|
IF v_section_code IS NOT NULL THEN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT v_pid, v.row_type, v.depth, v.sort_order, v.data
|
||||||
|
FROM v3.v_form4_sheet_jsonb(v_form_id, v_section_code, NULL) v
|
||||||
|
WHERE (v.row_type = 'ITEM' AND (v.data->'header'->>'project_id')::INT = v_pid)
|
||||||
|
OR (v.row_type = 'GROUP' AND (v.data->'header'->>'program_id')::INT = p_program_id)
|
||||||
|
OR (v.row_type = 'ROOT' AND v.data->'header'->>'section_code' = v_section_code)
|
||||||
|
ORDER BY v.sort_order;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql VOLATILE;
|
||||||
|
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- v3.v_form4_sheet_jsonb — JSONB-выдача FORM_4 для АХР_Р / КВ_Р / Операц
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Иерархия (4 уровня):
|
||||||
|
-- ROOT depth=0 — section_code (1.00.0/3.00.0/4.00.0/5.00.0)
|
||||||
|
-- PROGRAM depth=1 — v3.form4_project level=program
|
||||||
|
-- PROJECT depth=2 — v3.form4_project level=project (parent_id=program)
|
||||||
|
-- INPUT depth=3 — budget_line (R-код)
|
||||||
|
--
|
||||||
|
-- Финансовые блоки (plan, seq_dfip, approved, booking, q1-q4, totals)
|
||||||
|
-- агрегируются на уровнях ROOT/PROGRAM/PROJECT через v3.jsonb_numeric_sum.
|
||||||
|
-- Текстовые поля (counterparty, comment, justification…) на агрегатных
|
||||||
|
-- уровнях NULL'ятся (jsonb_numeric_sum их игнорирует).
|
||||||
|
--
|
||||||
|
-- p_form_id — id формы FORM_4
|
||||||
|
-- p_sheet — 'AHR' / 'CAP' / 'OPER'
|
||||||
|
-- p_sections — массив включаемых блоков (как в v_form4_sheet_lines_jsonb)
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- ───── 1. Хелпер: рекурсивная сумма численных листьев двух JSONB ──────────
|
||||||
|
CREATE OR REPLACE FUNCTION v3.jsonb_numeric_sum(a JSONB, b JSONB)
|
||||||
|
RETURNS JSONB LANGUAGE sql IMMUTABLE AS $function$
|
||||||
|
SELECT CASE
|
||||||
|
WHEN a IS NULL THEN b
|
||||||
|
WHEN b IS NULL THEN a
|
||||||
|
WHEN jsonb_typeof(a) = 'number' AND jsonb_typeof(b) = 'number' THEN
|
||||||
|
to_jsonb((a)::text::numeric + (b)::text::numeric)
|
||||||
|
WHEN jsonb_typeof(a) = 'object' AND jsonb_typeof(b) = 'object' THEN (
|
||||||
|
SELECT jsonb_object_agg(k,
|
||||||
|
v3.jsonb_numeric_sum(a -> k, b -> k)
|
||||||
|
)
|
||||||
|
FROM (
|
||||||
|
SELECT k FROM jsonb_object_keys(a) k
|
||||||
|
UNION
|
||||||
|
SELECT k FROM jsonb_object_keys(b) k
|
||||||
|
) keys
|
||||||
|
)
|
||||||
|
-- одна сторона число/объект, другая нет → берём числовую/объектную
|
||||||
|
WHEN jsonb_typeof(a) IN ('number','object') THEN a
|
||||||
|
WHEN jsonb_typeof(b) IN ('number','object') THEN b
|
||||||
|
ELSE NULL
|
||||||
|
END;
|
||||||
|
$function$;
|
||||||
|
|
||||||
|
DROP AGGREGATE IF EXISTS v3.jsonb_sum(JSONB);
|
||||||
|
CREATE AGGREGATE v3.jsonb_sum(JSONB) (
|
||||||
|
SFUNC = v3.jsonb_numeric_sum,
|
||||||
|
STYPE = JSONB
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ───── 2. Основная функция ─────────────────────────────────────────────────
|
||||||
|
CREATE OR REPLACE FUNCTION v3.v_form4_sheet_jsonb(
|
||||||
|
p_form_id INT,
|
||||||
|
p_sheet VARCHAR,
|
||||||
|
p_sections TEXT[] DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
row_type VARCHAR,
|
||||||
|
depth INT,
|
||||||
|
sort_order BIGINT,
|
||||||
|
data JSONB
|
||||||
|
)
|
||||||
|
AS $function$
|
||||||
|
-- ═══ 1. Базовые INPUT-строки с расширенной иерархией ═══════════════════════
|
||||||
|
WITH inp_raw AS (
|
||||||
|
SELECT j.*
|
||||||
|
FROM v3.v_form4_sheet_lines_jsonb(p_form_id, p_sheet, p_sections) j
|
||||||
|
WHERE j.row_type = 'INPUT'
|
||||||
|
),
|
||||||
|
inp AS (
|
||||||
|
SELECT
|
||||||
|
i.*,
|
||||||
|
bl.project_id AS line_project_id,
|
||||||
|
|
||||||
|
-- --- разрешение проекта ---
|
||||||
|
-- A. bl.project_id → project (level='project')
|
||||||
|
-- B. bl.project_id → program (level='program') — тогда проекта нет
|
||||||
|
CASE WHEN prj.id IS NOT NULL THEN prj.id END AS prj_id,
|
||||||
|
CASE WHEN prj.id IS NOT NULL THEN prj.name END AS prj_name,
|
||||||
|
|
||||||
|
-- --- разрешение программы ---
|
||||||
|
-- A. через проект: prog_via_prj
|
||||||
|
-- B. напрямую: prog_direct
|
||||||
|
COALESCE(prog_via_prj.id, prog_direct.id) AS prog_id,
|
||||||
|
COALESCE(prog_via_prj.name, prog_direct.name) AS prog_name,
|
||||||
|
|
||||||
|
-- --- section (depth=0) ---
|
||||||
|
sec.id AS sec_id,
|
||||||
|
sec.section_code AS sec_code,
|
||||||
|
sec.name AS sec_name
|
||||||
|
|
||||||
|
FROM inp_raw i
|
||||||
|
JOIN v3.budget_line bl ON bl.id = i.line_id
|
||||||
|
|
||||||
|
-- проект (level='project')
|
||||||
|
LEFT JOIN v3.form4_project prj
|
||||||
|
ON prj.id = bl.project_id AND prj.level = 'project'
|
||||||
|
|
||||||
|
-- программа через проект
|
||||||
|
LEFT JOIN v3.form4_project prog_via_prj
|
||||||
|
ON prog_via_prj.id = prj.parent_id AND prog_via_prj.level = 'program'
|
||||||
|
|
||||||
|
-- программа напрямую (bl.project_id → program)
|
||||||
|
LEFT JOIN v3.form4_project prog_direct
|
||||||
|
ON prog_direct.id = bl.project_id AND prog_direct.level = 'program'
|
||||||
|
|
||||||
|
-- раздел depth=0 (через expense_item)
|
||||||
|
JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
WITH RECURSIVE up AS (
|
||||||
|
SELECT id, parent_id, name, section_code, depth
|
||||||
|
FROM v3.expense_item WHERE id = ei.id
|
||||||
|
UNION ALL
|
||||||
|
SELECT e.id, e.parent_id, e.name, e.section_code, e.depth
|
||||||
|
FROM v3.expense_item e JOIN up u ON u.parent_id = e.id
|
||||||
|
)
|
||||||
|
SELECT id, name, section_code FROM up WHERE depth = 0 LIMIT 1
|
||||||
|
) sec ON TRUE
|
||||||
|
),
|
||||||
|
|
||||||
|
-- ═══ 2. Все программы (включая пустые) ═════════════════════════════════════
|
||||||
|
all_programs AS (
|
||||||
|
SELECT
|
||||||
|
fp.id AS prog_id,
|
||||||
|
fp.name AS prog_name,
|
||||||
|
fp.section_code,
|
||||||
|
sec.id AS sec_id,
|
||||||
|
sec.section_code AS sec_code,
|
||||||
|
sec.name AS sec_name
|
||||||
|
FROM v3.form4_project fp
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT id, section_code, name
|
||||||
|
FROM v3.expense_item
|
||||||
|
WHERE section_code = fp.section_code AND depth = 0
|
||||||
|
LIMIT 1
|
||||||
|
) sec ON TRUE
|
||||||
|
WHERE fp.level = 'program'
|
||||||
|
AND (p_sections IS NULL OR fp.section_code = ANY(p_sections))
|
||||||
|
AND (fp.section_code = p_sheet) AND fp.form_id = p_form_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- ═══ 3. Все проекты под программами (включая пустые) ═══════════════════════
|
||||||
|
all_projects AS (
|
||||||
|
SELECT
|
||||||
|
prj.id AS prj_id,
|
||||||
|
prj.name AS prj_name,
|
||||||
|
prj.parent_id AS prog_id,
|
||||||
|
ap.sec_id, ap.sec_code, ap.sec_name
|
||||||
|
FROM v3.form4_project prj
|
||||||
|
JOIN all_programs ap ON ap.prog_id = prj.parent_id
|
||||||
|
WHERE prj.level = 'project' AND (ap.section_code = p_sheet)
|
||||||
|
),
|
||||||
|
|
||||||
|
-- ═══ 4. Агрегаты с финансовыми данными (только из inp) ════════════════════
|
||||||
|
|
||||||
|
-- PROJECT-агрегаты
|
||||||
|
prj_agg AS (
|
||||||
|
SELECT
|
||||||
|
sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name,
|
||||||
|
prog_id, MAX(prog_name) AS prog_name,
|
||||||
|
prj_id, MAX(prj_name) AS prj_name,
|
||||||
|
v3.jsonb_sum(plan_data) AS plan_data,
|
||||||
|
v3.jsonb_sum(seq_dfip_data) AS seq_dfip_data,
|
||||||
|
v3.jsonb_sum(approved_data) AS approved_data,
|
||||||
|
v3.jsonb_sum(contract_summary_data) AS contract_summary_data,
|
||||||
|
v3.jsonb_sum(allocation_data) AS allocation_data,
|
||||||
|
v3.jsonb_sum(reserve_data) AS reserve_data,
|
||||||
|
v3.jsonb_sum(collegial_data) AS collegial_data,
|
||||||
|
v3.jsonb_sum(ckk_data) AS ckk_data,
|
||||||
|
v3.jsonb_sum(contract_data) AS contract_data,
|
||||||
|
v3.jsonb_sum(booking_data) AS booking_data,
|
||||||
|
v3.jsonb_sum(q1_data) AS q1_data,
|
||||||
|
v3.jsonb_sum(q2_data) AS q2_data,
|
||||||
|
v3.jsonb_sum(q3_data) AS q3_data,
|
||||||
|
v3.jsonb_sum(q4_data) AS q4_data,
|
||||||
|
v3.jsonb_sum(totals_data) AS totals_data
|
||||||
|
FROM inp WHERE prj_id IS NOT NULL
|
||||||
|
GROUP BY sec_id, prog_id, prj_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- PROGRAM-агрегаты
|
||||||
|
prog_agg AS (
|
||||||
|
SELECT
|
||||||
|
sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name,
|
||||||
|
prog_id, MAX(prog_name) AS prog_name,
|
||||||
|
v3.jsonb_sum(plan_data) AS plan_data,
|
||||||
|
v3.jsonb_sum(seq_dfip_data) AS seq_dfip_data,
|
||||||
|
v3.jsonb_sum(approved_data) AS approved_data,
|
||||||
|
v3.jsonb_sum(contract_summary_data) AS contract_summary_data,
|
||||||
|
v3.jsonb_sum(allocation_data) AS allocation_data,
|
||||||
|
v3.jsonb_sum(reserve_data) AS reserve_data,
|
||||||
|
v3.jsonb_sum(collegial_data) AS collegial_data,
|
||||||
|
v3.jsonb_sum(ckk_data) AS ckk_data,
|
||||||
|
v3.jsonb_sum(contract_data) AS contract_data,
|
||||||
|
v3.jsonb_sum(booking_data) AS booking_data,
|
||||||
|
v3.jsonb_sum(q1_data) AS q1_data,
|
||||||
|
v3.jsonb_sum(q2_data) AS q2_data,
|
||||||
|
v3.jsonb_sum(q3_data) AS q3_data,
|
||||||
|
v3.jsonb_sum(q4_data) AS q4_data,
|
||||||
|
v3.jsonb_sum(totals_data) AS totals_data
|
||||||
|
FROM inp WHERE prog_id IS NOT NULL
|
||||||
|
GROUP BY sec_id, prog_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- ROOT-агрегаты
|
||||||
|
root_agg AS (
|
||||||
|
SELECT
|
||||||
|
sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name,
|
||||||
|
v3.jsonb_sum(plan_data) AS plan_data,
|
||||||
|
v3.jsonb_sum(seq_dfip_data) AS seq_dfip_data,
|
||||||
|
v3.jsonb_sum(approved_data) AS approved_data,
|
||||||
|
v3.jsonb_sum(contract_summary_data) AS contract_summary_data,
|
||||||
|
v3.jsonb_sum(allocation_data) AS allocation_data,
|
||||||
|
v3.jsonb_sum(reserve_data) AS reserve_data,
|
||||||
|
v3.jsonb_sum(collegial_data) AS collegial_data,
|
||||||
|
v3.jsonb_sum(ckk_data) AS ckk_data,
|
||||||
|
v3.jsonb_sum(contract_data) AS contract_data,
|
||||||
|
v3.jsonb_sum(booking_data) AS booking_data,
|
||||||
|
v3.jsonb_sum(q1_data) AS q1_data,
|
||||||
|
v3.jsonb_sum(q2_data) AS q2_data,
|
||||||
|
v3.jsonb_sum(q3_data) AS q3_data,
|
||||||
|
v3.jsonb_sum(q4_data) AS q4_data,
|
||||||
|
v3.jsonb_sum(totals_data) AS totals_data
|
||||||
|
FROM inp
|
||||||
|
GROUP BY sec_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Нулевой JSONB-шаблон (для пустых программ/проектов)
|
||||||
|
zero AS (
|
||||||
|
SELECT '{}'::jsonb AS z
|
||||||
|
),
|
||||||
|
|
||||||
|
-- ═══ 5. UNION всех уровней ═════════════════════════════════════════════════
|
||||||
|
unioned AS (
|
||||||
|
-- ROOT — только из агрегатов (не имеет смысла пустой ROOT без строк)
|
||||||
|
SELECT
|
||||||
|
'ROOT'::VARCHAR AS row_type, 0 AS depth, NULL::INT AS line_id,
|
||||||
|
jsonb_build_object('section_code', sec_code, 'name', sec_name, 'expense_item_id', sec_id) AS header,
|
||||||
|
plan_data, seq_dfip_data, approved_data,
|
||||||
|
contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
|
||||||
|
contract_data, booking_data,
|
||||||
|
q1_data, q2_data, q3_data, q4_data, totals_data,
|
||||||
|
ARRAY[sec_id, 0, 0, 0]::INT[] AS sort_path
|
||||||
|
FROM root_agg
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- GROUP — из агрегатов (программы с бюджетными строками)
|
||||||
|
SELECT
|
||||||
|
'GROUP', 1, NULL,
|
||||||
|
jsonb_build_object('section_code', sec_code, 'name', prog_name, 'program_id', prog_id, 'expense_item_id', sec_id),
|
||||||
|
plan_data, seq_dfip_data, approved_data,
|
||||||
|
contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
|
||||||
|
contract_data, booking_data,
|
||||||
|
q1_data, q2_data, q3_data, q4_data, totals_data,
|
||||||
|
ARRAY[sec_id, prog_id, 0, 0]::INT[]
|
||||||
|
FROM prog_agg
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- GROUP — пустые программы (нет ни одной budget_line)
|
||||||
|
SELECT
|
||||||
|
'GROUP', 1, NULL,
|
||||||
|
jsonb_build_object('section_code', sec_code, 'name', prog_name, 'program_id', prog_id, 'expense_item_id', sec_id),
|
||||||
|
NULL::jsonb, NULL::jsonb, NULL::jsonb, -- plan, seq_dfip, approved
|
||||||
|
NULL::jsonb, NULL::jsonb, NULL::jsonb, -- contract_summary, allocation, reserve
|
||||||
|
NULL::jsonb, NULL::jsonb, -- collegial, ckk
|
||||||
|
NULL::jsonb, NULL::jsonb, -- contract, booking
|
||||||
|
NULL::jsonb, NULL::jsonb, NULL::jsonb, NULL::jsonb, NULL::jsonb, -- q1-q4, totals
|
||||||
|
ARRAY[sec_id, prog_id, 0, 0]::INT[]
|
||||||
|
FROM all_programs ap
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM v3.budget_line bl
|
||||||
|
WHERE bl.budget_form_id = p_form_id
|
||||||
|
AND (
|
||||||
|
bl.project_id = ap.prog_id -- прямая привязка к программе
|
||||||
|
OR bl.project_id IN (SELECT id FROM v3.form4_project pr -- через проект
|
||||||
|
WHERE pr.parent_id = ap.prog_id AND pr.level = 'project')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- ITEM — из агрегатов (проекты с бюджетными строками)
|
||||||
|
SELECT
|
||||||
|
'ITEM', 2, NULL,
|
||||||
|
jsonb_build_object('section_code', sec_code, 'name', prj_name,
|
||||||
|
'program_id', prog_id, 'project_id', prj_id, 'expense_item_id', sec_id),
|
||||||
|
plan_data, seq_dfip_data, approved_data,
|
||||||
|
contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
|
||||||
|
contract_data, booking_data,
|
||||||
|
q1_data, q2_data, q3_data, q4_data, totals_data,
|
||||||
|
ARRAY[sec_id, prog_id, prj_id, 0]::INT[]
|
||||||
|
FROM prj_agg
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- ITEM — пустые проекты (нет budget_line)
|
||||||
|
SELECT
|
||||||
|
'ITEM', 2, NULL,
|
||||||
|
jsonb_build_object('section_code', sec_code, 'name', prj_name,
|
||||||
|
'program_id', prog_id, 'project_id', prj_id, 'expense_item_id', sec_id),
|
||||||
|
NULL::jsonb, NULL::jsonb, NULL::jsonb,
|
||||||
|
NULL::jsonb, NULL::jsonb, NULL::jsonb,
|
||||||
|
NULL::jsonb, NULL::jsonb,
|
||||||
|
NULL::jsonb, NULL::jsonb,
|
||||||
|
NULL::jsonb, NULL::jsonb, NULL::jsonb, NULL::jsonb, NULL::jsonb,
|
||||||
|
ARRAY[sec_id, prog_id, prj_id, 0]::INT[]
|
||||||
|
FROM all_projects apr
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM v3.budget_line bl
|
||||||
|
WHERE bl.budget_form_id = p_form_id
|
||||||
|
AND bl.project_id = apr.prj_id
|
||||||
|
)
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- INPUT — все строки бюджета
|
||||||
|
SELECT
|
||||||
|
'INPUT', 3, line_id,
|
||||||
|
header || jsonb_build_object(
|
||||||
|
'project_id', prj_id, 'project_name', prj_name,
|
||||||
|
'program_id', prog_id, 'program_name', prog_name
|
||||||
|
),
|
||||||
|
plan_data, seq_dfip_data, approved_data,
|
||||||
|
contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
|
||||||
|
contract_data, booking_data,
|
||||||
|
q1_data, q2_data, q3_data, q4_data, totals_data,
|
||||||
|
ARRAY[sec_id, COALESCE(prog_id, 999999), COALESCE(prj_id, 999999), line_id]::INT[]
|
||||||
|
FROM inp
|
||||||
|
)
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
u.row_type, u.depth,
|
||||||
|
ROW_NUMBER() OVER (ORDER BY u.sort_path) AS sort_order,
|
||||||
|
jsonb_build_object(
|
||||||
|
'line_id', u.line_id,
|
||||||
|
'header', u.header,
|
||||||
|
'plan', u.plan_data,
|
||||||
|
'seq_dfip', u.seq_dfip_data,
|
||||||
|
'approved', u.approved_data,
|
||||||
|
'contract_summary', u.contract_summary_data,
|
||||||
|
'allocation', u.allocation_data,
|
||||||
|
'reserve', u.reserve_data,
|
||||||
|
'collegial', u.collegial_data,
|
||||||
|
'ckk', u.ckk_data,
|
||||||
|
'contract', u.contract_data,
|
||||||
|
'booking', u.booking_data,
|
||||||
|
'q1', u.q1_data,
|
||||||
|
'q2', u.q2_data,
|
||||||
|
'q3', u.q3_data,
|
||||||
|
'q4', u.q4_data,
|
||||||
|
'totals', u.totals_data
|
||||||
|
) AS data
|
||||||
|
FROM unioned u
|
||||||
|
ORDER BY u.sort_path;
|
||||||
|
$function$
|
||||||
|
LANGUAGE sql STABLE;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- v3.add_budget_line — создать новую INPUT-строку (budget_line) для FORM_1/2/4.
|
||||||
|
--
|
||||||
|
-- Способы передать expense_item:
|
||||||
|
-- путь A — численный id: p_expense_item_id (берётся из header.expense_item_id)
|
||||||
|
-- путь B — резолв по коду: p_sheet + p_item_id [+ p_section_code для коллизий]
|
||||||
|
-- [+ p_direction для FORM_1]
|
||||||
|
--
|
||||||
|
-- Авто-direction:
|
||||||
|
-- FORM_2 → 'Support'
|
||||||
|
-- FORM_4 → 'Development'
|
||||||
|
-- FORM_1/OPER → NULL
|
||||||
|
-- FORM_1/AHR|CAP → p_direction обязателен (Support|Development)
|
||||||
|
-- Если p_direction передан и противоречит авто-значению — ошибка.
|
||||||
|
--
|
||||||
|
-- Дубликаты на одном expense_item разрешены (UNIQUE не накладывается).
|
||||||
|
-- vsp_id опционален — для FORM_2 фронт может не знать ВСП на момент
|
||||||
|
-- создания, дозаполнит позже через upd_form_cell(.., 'header.vsp_id', ..).
|
||||||
|
--
|
||||||
|
-- FORM_3 не поддерживается — там строки живут в rf_project_report_line,
|
||||||
|
-- API будет в add_form3_line.sql (отдельная итерация).
|
||||||
|
--
|
||||||
|
-- Возврат: вся иерархия (ROOT/GROUP/ITEM/SUB_ITEM/SECTION/LEAF) + новая
|
||||||
|
-- INPUT (отфильтрована по новому line_id). Новый id извлекается из
|
||||||
|
-- (data->>'line_id')::INT первой INPUT-строки.
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS v3.add_budget_line(INT, INT, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, INT, VARCHAR);
|
||||||
|
DROP FUNCTION IF EXISTS v3.add_budget_line(INT, INT, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, INT, VARCHAR, VARCHAR, DATE);
|
||||||
|
|
||||||
|
CREATE FUNCTION v3.add_budget_line(
|
||||||
|
p_form_id INT,
|
||||||
|
p_expense_item_id INT DEFAULT NULL,
|
||||||
|
p_sheet VARCHAR DEFAULT NULL,
|
||||||
|
p_item_id VARCHAR DEFAULT NULL,
|
||||||
|
p_section_code VARCHAR DEFAULT NULL,
|
||||||
|
p_direction VARCHAR DEFAULT NULL,
|
||||||
|
p_name VARCHAR DEFAULT NULL,
|
||||||
|
p_internal_order VARCHAR DEFAULT NULL,
|
||||||
|
p_vsp_id INT DEFAULT NULL,
|
||||||
|
p_project_id INT DEFAULT NULL,
|
||||||
|
p_justification VARCHAR DEFAULT NULL,
|
||||||
|
-- сателлитные листы (AHR_RENT/UTILITY/SECURITY) используют дополнительно:
|
||||||
|
p_contract_number VARCHAR DEFAULT NULL,
|
||||||
|
p_contract_end_date DATE DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
row_type VARCHAR,
|
||||||
|
depth INT,
|
||||||
|
sort_order BIGINT,
|
||||||
|
data JSONB
|
||||||
|
)
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_form_type VARCHAR;
|
||||||
|
v_eid INT;
|
||||||
|
v_sheet VARCHAR;
|
||||||
|
v_auto_dir VARCHAR;
|
||||||
|
v_final_dir VARCHAR;
|
||||||
|
v_new_id INT;
|
||||||
|
v_cnt INT;
|
||||||
|
v_sat_table TEXT;
|
||||||
|
v_sat_eid INT;
|
||||||
|
v_parent_line INT;
|
||||||
|
BEGIN
|
||||||
|
-- 1. form_id → form_type
|
||||||
|
SELECT form_type_code INTO v_form_type FROM v3.budget_form WHERE id = p_form_id;
|
||||||
|
IF v_form_type IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'budget_form id=% не существует', p_form_id;
|
||||||
|
END IF;
|
||||||
|
IF v_form_type = 'FORM_3' THEN
|
||||||
|
RAISE EXCEPTION 'FORM_3 не использует budget_line — см. add_form3_line (TBD)';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ═══ Сателлитные листы (AHR_RENT/UTILITY/SECURITY) ═══════════════════
|
||||||
|
-- Фиксированный expense_item_id на лист — UI не передаёт R-код.
|
||||||
|
-- p_vsp_id обязателен. Опц.: p_contract_number, p_contract_end_date.
|
||||||
|
-- Внутри: находим/создаём родительскую budget_line с фиксированным ei,
|
||||||
|
-- затем INSERT в *_detail. Возврат — одна новая LEAF.
|
||||||
|
IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN
|
||||||
|
v_sat_table := CASE p_sheet
|
||||||
|
WHEN 'AHR_RENT' THEN 'rent_detail'
|
||||||
|
WHEN 'AHR_UTILITY' THEN 'utility_detail'
|
||||||
|
WHEN 'AHR_SECURITY' THEN 'security_detail'
|
||||||
|
END;
|
||||||
|
v_sat_eid := CASE p_sheet
|
||||||
|
WHEN 'AHR_RENT' THEN 293 -- R061031001 "Аренда_ОН"
|
||||||
|
WHEN 'AHR_UTILITY' THEN 24 -- 1.05.1. "Коммунальные услуги"
|
||||||
|
WHEN 'AHR_SECURITY' THEN 779 -- R061062001 "Пультовая охр.ОН"
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF p_vsp_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_vsp_id обязателен для сателлитного листа %', p_sheet;
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN
|
||||||
|
RAISE EXCEPTION 'vsp id=% не существует', p_vsp_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Находим/создаём родительскую budget_line под фиксированным ei
|
||||||
|
SELECT id INTO v_parent_line
|
||||||
|
FROM v3.budget_line
|
||||||
|
WHERE budget_form_id = p_form_id AND expense_item_id = v_sat_eid
|
||||||
|
LIMIT 1;
|
||||||
|
IF v_parent_line IS NULL THEN
|
||||||
|
INSERT INTO v3.budget_line (budget_form_id, expense_item_id, direction)
|
||||||
|
VALUES (
|
||||||
|
p_form_id, v_sat_eid,
|
||||||
|
CASE v_form_type WHEN 'FORM_2' THEN 'Support' WHEN 'FORM_4' THEN 'Development' ELSE NULL END
|
||||||
|
) RETURNING id INTO v_parent_line;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- INSERT в сателлит
|
||||||
|
EXECUTE format(
|
||||||
|
'INSERT INTO v3.%I (line_id, vsp_id, contract_number, contract_end_date) '
|
||||||
|
|| 'VALUES ($1, $2, $3, $4) RETURNING id',
|
||||||
|
v_sat_table
|
||||||
|
) USING v_parent_line, p_vsp_id, p_contract_number, p_contract_end_date
|
||||||
|
INTO v_new_id;
|
||||||
|
|
||||||
|
-- Возврат — одна новая INPUT-строка (отфильтрована по новому id сателлита)
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT v.row_type, v.depth, v.sort_order, v.data
|
||||||
|
FROM v3.v_form_view(p_form_id, p_sheet, NULL, NULL) v
|
||||||
|
WHERE v.row_type = 'INPUT' AND (v.data->>'id')::INT = v_new_id;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 2. Резолв expense_item_id
|
||||||
|
IF p_expense_item_id IS NOT NULL THEN
|
||||||
|
SELECT ei.id, ei.sheet INTO v_eid, v_sheet
|
||||||
|
FROM v3.expense_item ei WHERE ei.id = p_expense_item_id;
|
||||||
|
IF v_eid IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'expense_item id=% не существует', p_expense_item_id;
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM v3.expense_item_form_type
|
||||||
|
WHERE expense_item_id = v_eid AND form_type_code = v_form_type
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION 'expense_item id=% не привязан к form_type %', v_eid, v_form_type;
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
IF p_sheet IS NULL OR p_item_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'нужен либо p_expense_item_id, либо (p_sheet + p_item_id)';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT count(*) INTO v_cnt
|
||||||
|
FROM v3.expense_item ei
|
||||||
|
JOIN v3.expense_item_form_type eift ON eift.expense_item_id = ei.id
|
||||||
|
WHERE ei.item_id = p_item_id
|
||||||
|
AND ei.sheet = p_sheet
|
||||||
|
AND eift.form_type_code = v_form_type
|
||||||
|
AND (p_section_code IS NULL OR ei.section_code = p_section_code)
|
||||||
|
AND (p_direction IS NULL OR ei.direction IS NULL OR ei.direction = p_direction);
|
||||||
|
|
||||||
|
IF v_cnt = 0 THEN
|
||||||
|
RAISE EXCEPTION 'expense_item не найден: sheet=%, item_id=%, section=%, direction=%, form=%',
|
||||||
|
p_sheet, p_item_id, p_section_code, p_direction, v_form_type;
|
||||||
|
END IF;
|
||||||
|
IF v_cnt > 1 THEN
|
||||||
|
RAISE EXCEPTION 'expense_item не уникален (% совпадений): sheet=%, item_id=%, section=%, direction=%. Уточните p_section_code или передайте p_expense_item_id напрямую',
|
||||||
|
v_cnt, p_sheet, p_item_id, p_section_code, p_direction;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT ei.id, ei.sheet INTO v_eid, v_sheet
|
||||||
|
FROM v3.expense_item ei
|
||||||
|
JOIN v3.expense_item_form_type eift ON eift.expense_item_id = ei.id
|
||||||
|
WHERE ei.item_id = p_item_id
|
||||||
|
AND ei.sheet = p_sheet
|
||||||
|
AND eift.form_type_code = v_form_type
|
||||||
|
AND (p_section_code IS NULL OR ei.section_code = p_section_code)
|
||||||
|
AND (p_direction IS NULL OR ei.direction IS NULL OR ei.direction = p_direction);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 3. Авто-direction (для FORM_1/AHR|CAP — обязателен у юзера)
|
||||||
|
v_auto_dir := CASE
|
||||||
|
WHEN v_form_type = 'FORM_2' THEN 'Support'
|
||||||
|
WHEN v_form_type = 'FORM_4' THEN 'Development'
|
||||||
|
WHEN v_form_type = 'FORM_1' AND v_sheet = 'OPER' THEN NULL
|
||||||
|
ELSE NULL
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF v_form_type = 'FORM_1' AND v_sheet IN ('AHR','CAP') AND p_direction IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_direction обязателен для FORM_1 / sheet=% (Support|Development)', v_sheet;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_form_type IN ('FORM_2','FORM_4') AND p_direction IS NOT NULL AND p_direction <> v_auto_dir THEN
|
||||||
|
RAISE EXCEPTION 'p_direction=% несовместим с form_type=% (фиксирован=%)',
|
||||||
|
p_direction, v_form_type, v_auto_dir;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_form_type = 'FORM_1' AND v_sheet = 'OPER' AND p_direction IS NOT NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_direction должен быть NULL для FORM_1 / sheet=OPER (получено: %)', p_direction;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
v_final_dir := COALESCE(p_direction, v_auto_dir);
|
||||||
|
|
||||||
|
-- 4. Sanity на vsp/project
|
||||||
|
IF p_vsp_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN
|
||||||
|
RAISE EXCEPTION 'vsp id=% не существует', p_vsp_id;
|
||||||
|
END IF;
|
||||||
|
IF p_project_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.form4_project WHERE id = p_project_id) THEN
|
||||||
|
RAISE EXCEPTION 'form4_project id=% не существует', p_project_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 5. INSERT
|
||||||
|
INSERT INTO v3.budget_line (
|
||||||
|
budget_form_id, expense_item_id, name, internal_order,
|
||||||
|
vsp_id, project_id, direction, justification
|
||||||
|
) VALUES (
|
||||||
|
p_form_id, v_eid, p_name, p_internal_order,
|
||||||
|
p_vsp_id, p_project_id, v_final_dir, p_justification
|
||||||
|
) RETURNING id INTO v_new_id;
|
||||||
|
|
||||||
|
-- 5а. Аудит: ROW_CREATE
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'ROW_CREATE', 'ROW_CREATE',
|
||||||
|
jsonb_build_object(
|
||||||
|
'sheet', v_sheet,
|
||||||
|
'line_id', v_new_id,
|
||||||
|
'expense_item_id', v_eid,
|
||||||
|
'direction', v_final_dir,
|
||||||
|
'name', p_name,
|
||||||
|
'vsp_id', p_vsp_id,
|
||||||
|
'project_id', p_project_id
|
||||||
|
),
|
||||||
|
p_form_id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 6. Возврат: только иерархия по пути нового ei + новая INPUT
|
||||||
|
DECLARE v_ei_path INT[];
|
||||||
|
BEGIN
|
||||||
|
v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]);
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT v.row_type, v.depth, v.sort_order, v.data
|
||||||
|
FROM v3.v_form_view(p_form_id, v_sheet, NULL, v_final_dir) v
|
||||||
|
WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
|
||||||
|
AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
|
||||||
|
OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = v_new_id)
|
||||||
|
ORDER BY v.sort_order;
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql VOLATILE;
|
||||||
@ -14,6 +14,7 @@ from sqlalchemy.exc import IntegrityError
|
|||||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status
|
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.repository.sheet_repository import SheetValidationError
|
||||||
from src.services.rf_project_report_service import RfProjectReportService
|
from src.services.rf_project_report_service import RfProjectReportService
|
||||||
from src.services.rf_project_report_line_service import RfProjectReportLineService
|
from src.services.rf_project_report_line_service import RfProjectReportLineService
|
||||||
from src.repository.user_repository import UserRepository
|
from src.repository.user_repository import UserRepository
|
||||||
@ -376,6 +377,18 @@ class FormEventProcess:
|
|||||||
sheet=sheet,
|
sheet=sheet,
|
||||||
direction=direction,
|
direction=direction,
|
||||||
)
|
)
|
||||||
|
case "program_added":
|
||||||
|
return await self.__add_program(
|
||||||
|
event_data=event_data["data"],
|
||||||
|
form_id=form_id,
|
||||||
|
user=curr_user,
|
||||||
|
sheet=sheet,
|
||||||
|
)
|
||||||
|
case "project_added":
|
||||||
|
return await self.__add_project(
|
||||||
|
event_data=event_data["data"],
|
||||||
|
user=curr_user,
|
||||||
|
)
|
||||||
case _:
|
case _:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -461,8 +474,43 @@ class FormEventProcess:
|
|||||||
final_result["new_line_id"] = el[3]["line_id"]
|
final_result["new_line_id"] = el[3]["line_id"]
|
||||||
return final_result
|
return final_result
|
||||||
return final_result
|
return final_result
|
||||||
|
|
||||||
|
async def __add_program(
|
||||||
|
self,
|
||||||
|
event_data: dict,
|
||||||
|
form_id: int,
|
||||||
|
sheet: str,
|
||||||
|
user: AppUser,
|
||||||
|
) -> list[tuple]:
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
name: str
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
return await self.sheet_service.add_program(
|
||||||
|
form_id=form_id,
|
||||||
|
sheet=sheet,
|
||||||
|
name=event_data["name"],
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def __add_project(
|
||||||
|
self,
|
||||||
|
event_data: dict,
|
||||||
|
user: AppUser,
|
||||||
|
) -> list[tuple]:
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
name: str,
|
||||||
|
program_id: int
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
return await self.sheet_service.add_project(
|
||||||
|
name=event_data["name"],
|
||||||
|
program_id=event_data["program_id"],
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
|
||||||
async def __del_row(
|
async def __del_row(
|
||||||
self,
|
self,
|
||||||
event_data: dict,
|
event_data: dict,
|
||||||
@ -824,6 +872,8 @@ async def process_websocket(
|
|||||||
if data.get("event") in [
|
if data.get("event") in [
|
||||||
"cell_updated",
|
"cell_updated",
|
||||||
"row_added",
|
"row_added",
|
||||||
|
"program_added",
|
||||||
|
"project_added",
|
||||||
"row_deleted",
|
"row_deleted",
|
||||||
]:
|
]:
|
||||||
await manager.broadcast_to_all(
|
await manager.broadcast_to_all(
|
||||||
@ -852,6 +902,12 @@ async def process_websocket(
|
|||||||
data=data,
|
data=data,
|
||||||
websocket=websocket,
|
websocket=websocket,
|
||||||
)
|
)
|
||||||
|
except SheetValidationError as e:
|
||||||
|
data["error"] = e.message
|
||||||
|
await manager.send_back(
|
||||||
|
data=data,
|
||||||
|
websocket=websocket,
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
tp = type(e)
|
tp = type(e)
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from src.db.models.contract_detail import ContractDetail
|
|||||||
from src.db.models.contract_summary import ContractSummary
|
from src.db.models.contract_summary import ContractSummary
|
||||||
from src.db.models.expense_item import ExpenseItem, ExpenseItemFormType
|
from src.db.models.expense_item import ExpenseItem, ExpenseItemFormType
|
||||||
from src.db.models.form3_phase import Form3Phase
|
from src.db.models.form3_phase import Form3Phase
|
||||||
|
from src.db.models.form4_project import Form4Project
|
||||||
from src.db.models.form_limit import FormLimit
|
from src.db.models.form_limit import FormLimit
|
||||||
from src.db.models.form_phase import FormPhase
|
from src.db.models.form_phase import FormPhase
|
||||||
from src.db.models.form_type import FormType
|
from src.db.models.form_type import FormType
|
||||||
|
|||||||
54
api/src/db/models/form4_project.py
Normal file
54
api/src/db/models/form4_project.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import enum
|
||||||
|
|
||||||
|
from sqlalchemy import CheckConstraint, ForeignKey, Index, Integer, String, text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from src.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Form4ProjectLevelEnum(str, enum.Enum):
|
||||||
|
PROGRAM = "program"
|
||||||
|
PROJECT = "project"
|
||||||
|
|
||||||
|
|
||||||
|
class Form4Project(Base):
|
||||||
|
__tablename__ = "form4_project"
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint(
|
||||||
|
"level IN ('program', 'project')", name="chk_form4_project_level"
|
||||||
|
),
|
||||||
|
CheckConstraint(
|
||||||
|
"(level = 'program' AND parent_id IS NULL) OR (level = 'project')",
|
||||||
|
name="chk_form4_project_parent",
|
||||||
|
),
|
||||||
|
CheckConstraint(
|
||||||
|
"(level = 'program') = (section_code IS NOT NULL)",
|
||||||
|
name="chk_form4_project_section_code",
|
||||||
|
),
|
||||||
|
Index(
|
||||||
|
"ix_form4_project_section_code",
|
||||||
|
"section_code",
|
||||||
|
postgresql_where=text("level = 'program'"),
|
||||||
|
),
|
||||||
|
Index("ix_v3_form4_project_parent", "parent_id"),
|
||||||
|
Index("ix_v3_form4_project_form", "form_id"),
|
||||||
|
{"schema": "v3"},
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String)
|
||||||
|
level: Mapped[Form4ProjectLevelEnum] = mapped_column(String) # type: ignore
|
||||||
|
section_code: Mapped[str | None] = mapped_column(String)
|
||||||
|
parent_id: Mapped[int | None] = mapped_column(
|
||||||
|
Integer, ForeignKey("v3.form4_project.id")
|
||||||
|
)
|
||||||
|
form_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("v3.budget_form.id")
|
||||||
|
)
|
||||||
|
|
||||||
|
# parent = relationship("Form4Project", remote_side="Form4Project.id", back_populates="children")
|
||||||
|
# children: Mapped[list["Form4Project"]] = relationship("Form4Project", back_populates="parent") # type: ignore
|
||||||
|
# budget_form = relationship("BudgetForm")
|
||||||
|
# budget_lines: Mapped[list["BudgetLine"]] = relationship("BudgetLine", back_populates="form4_project") # type: ignore
|
||||||
65
api/src/repository/form4_project_repository.py
Normal file
65
api/src/repository/form4_project_repository.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
from sqlalchemy import func, select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.form4_project import Form4Project, Form4ProjectLevelEnum
|
||||||
|
|
||||||
|
|
||||||
|
class Form4ProjectRepository:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self,
|
||||||
|
project_id: int,
|
||||||
|
level: Form4ProjectLevelEnum | str | None = None,
|
||||||
|
) -> Form4Project | None:
|
||||||
|
where = [Form4Project.id == project_id]
|
||||||
|
if level is not None:
|
||||||
|
where.append(Form4Project.level == level)
|
||||||
|
|
||||||
|
return (
|
||||||
|
(await self.db.execute(select(Form4Project).where(*where).limit(1)))
|
||||||
|
.scalars()
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_list(
|
||||||
|
self,
|
||||||
|
level: Form4ProjectLevelEnum | None = None,
|
||||||
|
form_id: int | None = None,
|
||||||
|
parent_id: int | None = None,
|
||||||
|
offset: int | None = None,
|
||||||
|
limit: int | None = None,
|
||||||
|
with_count: bool = False,
|
||||||
|
) -> list[Form4Project] | tuple[int, list[Form4Project]]:
|
||||||
|
if with_count:
|
||||||
|
query = select(func.count().over().label("total_count"), Form4Project)
|
||||||
|
else:
|
||||||
|
query = select(Form4Project)
|
||||||
|
|
||||||
|
where = []
|
||||||
|
if level is not None:
|
||||||
|
where.append(Form4Project.level == level)
|
||||||
|
if form_id is not None:
|
||||||
|
where.append(Form4Project.form_id == form_id)
|
||||||
|
if parent_id is not None:
|
||||||
|
where.append(Form4Project.parent_id == parent_id)
|
||||||
|
|
||||||
|
query = query.where(*where).order_by(Form4Project.id)
|
||||||
|
|
||||||
|
if offset is not None:
|
||||||
|
query = query.offset(offset)
|
||||||
|
if limit is not None:
|
||||||
|
query = query.limit(limit)
|
||||||
|
|
||||||
|
if with_count:
|
||||||
|
result = (await self.db.execute(query)).all()
|
||||||
|
if result:
|
||||||
|
return result[0][0], [res[1] for res in result]
|
||||||
|
else:
|
||||||
|
count_query = select(func.count(Form4Project.id))
|
||||||
|
if query.whereclause is not None:
|
||||||
|
count_query = count_query.where(query.whereclause)
|
||||||
|
return (await self.db.execute(count_query)).scalar(), []
|
||||||
|
else:
|
||||||
|
return (await self.db.execute(query)).scalars().all()
|
||||||
@ -160,6 +160,59 @@ class SheetRepository:
|
|||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
async def add_program(
|
||||||
|
self,
|
||||||
|
form_id: int,
|
||||||
|
sheet: str,
|
||||||
|
name: str,
|
||||||
|
) -> list[tuple]:
|
||||||
|
return [
|
||||||
|
tuple(r) for r in (
|
||||||
|
await self.db.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT project_id, row_type, depth, sort_order, data
|
||||||
|
FROM v3.add_form4_program(
|
||||||
|
:p_name,
|
||||||
|
:p_form_id,
|
||||||
|
:p_sheet
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"p_form_id": form_id,
|
||||||
|
"p_name": name,
|
||||||
|
"p_sheet": sheet,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
]
|
||||||
|
|
||||||
|
async def add_project(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
program_id: int,
|
||||||
|
) -> list[tuple]:
|
||||||
|
return [
|
||||||
|
tuple(r) for r in (
|
||||||
|
await self.db.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT project_id, row_type, depth, sort_order, data
|
||||||
|
FROM v3.add_form4_project(
|
||||||
|
:p_name,
|
||||||
|
:p_program_id
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"p_program_id": program_id,
|
||||||
|
"p_name": name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
]
|
||||||
|
|
||||||
async def add_line(
|
async def add_line(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.form4_project import Form4ProjectLevelEnum
|
||||||
|
from src.db.models.role import UserRoleEnum
|
||||||
|
from src.repository.budget_form_repository import BudgetFormRepository
|
||||||
|
from src.repository.form4_project_repository import Form4ProjectRepository
|
||||||
|
from src.repository.user_repository import UserRepository
|
||||||
from src.db.models.app_user import AppUser
|
from src.db.models.app_user import AppUser
|
||||||
from src.db.models.budget_form import BudgetForm
|
from src.db.models.budget_form import BudgetForm
|
||||||
from src.repository.sheet_repository import SheetRepository
|
from src.repository.sheet_repository import SheetRepository, SheetValidationError
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -11,6 +16,9 @@ class SheetService:
|
|||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
self.db = db
|
self.db = db
|
||||||
self.sheet_repo = SheetRepository(db)
|
self.sheet_repo = SheetRepository(db)
|
||||||
|
self.bf_repo = BudgetFormRepository(db)
|
||||||
|
self.user_repo = UserRepository(db)
|
||||||
|
self.f4_project_repo = Form4ProjectRepository(db)
|
||||||
|
|
||||||
async def get(
|
async def get(
|
||||||
self,
|
self,
|
||||||
@ -26,28 +34,6 @@ class SheetService:
|
|||||||
direction=direction,
|
direction=direction,
|
||||||
sections=sections,
|
sections=sections,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def update_cell(
|
|
||||||
self,
|
|
||||||
form_id: int,
|
|
||||||
sheet: str,
|
|
||||||
direction: str | None,
|
|
||||||
sections: list[str] | None,
|
|
||||||
line_id: int,
|
|
||||||
column: str,
|
|
||||||
value: str | int | float | None = None,
|
|
||||||
user: AppUser | None = None,
|
|
||||||
) -> list[tuple]:
|
|
||||||
return await self.sheet_repo.update_cell(
|
|
||||||
form_id=form_id,
|
|
||||||
sheet=sheet,
|
|
||||||
direction=direction,
|
|
||||||
sections=sections,
|
|
||||||
line_id=line_id,
|
|
||||||
column=column,
|
|
||||||
value=value,
|
|
||||||
user_id=user.id if user else None,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def update_cells(
|
async def update_cells(
|
||||||
self,
|
self,
|
||||||
@ -123,6 +109,66 @@ class SheetService:
|
|||||||
contract_end_date=contract_end_date,
|
contract_end_date=contract_end_date,
|
||||||
user_id=user.id if user else None,
|
user_id=user.id if user else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def add_program(
|
||||||
|
self,
|
||||||
|
form_id: int,
|
||||||
|
sheet: str,
|
||||||
|
name: str,
|
||||||
|
user: AppUser | None = None,
|
||||||
|
) -> tuple[int, list[tuple]]:
|
||||||
|
if user.role_id != UserRoleEnum.ADMIN:
|
||||||
|
user = await self.user_repo.get(
|
||||||
|
user_id=user.id,
|
||||||
|
load_orgs=True,
|
||||||
|
)
|
||||||
|
form = await self.bf_repo.get(
|
||||||
|
budget_form_id=form_id,
|
||||||
|
org_unit=[ou.id for ou in user.org_units],
|
||||||
|
)
|
||||||
|
if not form:
|
||||||
|
raise SheetValidationError("Форма не найдена")
|
||||||
|
|
||||||
|
result = await self.sheet_repo.add_program(
|
||||||
|
form_id=form_id,
|
||||||
|
sheet=sheet,
|
||||||
|
name=name,
|
||||||
|
)
|
||||||
|
if result:
|
||||||
|
return (result[0][0], [tuple(r[1:]) for r in result])
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def add_project(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
program_id: int,
|
||||||
|
user: AppUser | None = None,
|
||||||
|
) -> tuple[int, list[tuple]]:
|
||||||
|
if user.role_id != UserRoleEnum.ADMIN:
|
||||||
|
program = await self.f4_project_repo.get(
|
||||||
|
project_id=program_id,
|
||||||
|
level=Form4ProjectLevelEnum.PROGRAM,
|
||||||
|
)
|
||||||
|
if not program:
|
||||||
|
raise SheetValidationError("Программа не найдена")
|
||||||
|
|
||||||
|
user = await self.user_repo.get(
|
||||||
|
user_id=user.id,
|
||||||
|
load_orgs=True,
|
||||||
|
)
|
||||||
|
form = await self.bf_repo.get(
|
||||||
|
budget_form_id=program.form_id,
|
||||||
|
org_unit=[ou.id for ou in user.org_units],
|
||||||
|
)
|
||||||
|
if not form:
|
||||||
|
raise SheetValidationError("Программа не найдена")
|
||||||
|
result = await self.sheet_repo.add_project(
|
||||||
|
program_id=program_id,
|
||||||
|
name=name,
|
||||||
|
)
|
||||||
|
if result:
|
||||||
|
return (result[0][0], [tuple(r[1:]) for r in result])
|
||||||
|
return None
|
||||||
|
|
||||||
async def delete_line(
|
async def delete_line(
|
||||||
self,
|
self,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user