Compare commits
8 Commits
97934a3404
...
6f0b3eb0cc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f0b3eb0cc | ||
| bdb4c389ce | |||
| 5e6ad47128 | |||
| 19dd443146 | |||
| 155888f7fc | |||
| 2e781ca809 | |||
| 4ec4936a0f | |||
| 88a9713fb3 |
@ -762,6 +762,43 @@ def upgrade() -> None:
|
|||||||
['code'],
|
['code'],
|
||||||
schema='v3',
|
schema='v3',
|
||||||
)
|
)
|
||||||
|
create_table_if_not_exists(
|
||||||
|
'form3_phase',
|
||||||
|
sa.Column('rf_project_report_id', sa.Integer(),primary_key=True),
|
||||||
|
sa.Column('phase_code', sa.String(), primary_key=True),
|
||||||
|
sa.Column('role', sa.String(), nullable=False),
|
||||||
|
sa.Column('column_keys', postgresql.ARRAY(sa.Text()), nullable=False),
|
||||||
|
sa.Column('opens_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('closes_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
schema='v3',
|
||||||
|
)
|
||||||
|
|
||||||
|
create_check_constraint_if_not_exists(
|
||||||
|
'chk_v3_form3_phase_window', 'form3_phase',
|
||||||
|
sa.text('closes_at > opens_at'),
|
||||||
|
schema='v3',
|
||||||
|
)
|
||||||
|
create_check_constraint_if_not_exists(
|
||||||
|
'chk_v3_form3_phase_columns', 'form3_phase',
|
||||||
|
sa.text('cardinality(column_keys) > 0'),
|
||||||
|
schema='v3',
|
||||||
|
)
|
||||||
|
create_check_constraint_if_not_exists(
|
||||||
|
'chk_v3_form3_phase_no_admin', 'form3_phase',
|
||||||
|
sa.text("role != 'ADMIN'"),
|
||||||
|
schema='v3',
|
||||||
|
)
|
||||||
|
create_index_if_not_exists(
|
||||||
|
'ix_v3_form3_phase_active', 'form3_phase',
|
||||||
|
['rf_project_report_id', 'role', 'opens_at', 'closes_at'],
|
||||||
|
schema='v3',
|
||||||
|
)
|
||||||
|
create_index_if_not_exists(
|
||||||
|
'ix_v3_form3_phase_columns_gin', 'form3_phase',
|
||||||
|
['column_keys'],
|
||||||
|
schema='v3',
|
||||||
|
postgresql_using='gin',
|
||||||
|
)
|
||||||
create_table_if_not_exists(
|
create_table_if_not_exists(
|
||||||
'security_detail',
|
'security_detail',
|
||||||
sa.Column('id', sa.Integer(),
|
sa.Column('id', sa.Integer(),
|
||||||
|
|||||||
@ -7772,4 +7772,407 @@ BEGIN
|
|||||||
|
|
||||||
END;
|
END;
|
||||||
$function$
|
$function$
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.copy_template_to_report(p_report_id INT)
|
||||||
|
RETURNS INTEGER
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_report_type VARCHAR;
|
||||||
|
v_inserted INTEGER;
|
||||||
|
BEGIN
|
||||||
|
SELECT report_type INTO v_report_type
|
||||||
|
FROM v3.rf_project_report
|
||||||
|
WHERE id = p_report_id;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
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, pt.closes_at
|
||||||
|
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$
|
||||||
|
LANGUAGE plpgsql VOLATILE;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.user_in_report_org(p_user_id INT, p_report_id INT)
|
||||||
|
RETURNS BOOLEAN
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_org INT;
|
||||||
|
BEGIN
|
||||||
|
SELECT p.org_unit_id
|
||||||
|
INTO v_org
|
||||||
|
FROM v3.rf_project_report r
|
||||||
|
JOIN v3.project p ON p.id = r.project_id
|
||||||
|
WHERE r.id = p_report_id;
|
||||||
|
|
||||||
|
IF v_org IS NULL THEN
|
||||||
|
RETURN TRUE; -- проект без org_unit — пропускаем фильтр
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM v3.user_org
|
||||||
|
WHERE user_id = p_user_id
|
||||||
|
AND org_unit_id = v_org
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql STABLE;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.editable_columns_for3(
|
||||||
|
p_report_id INT,
|
||||||
|
p_user_id INT
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
column_key TEXT,
|
||||||
|
closes_at TIMESTAMPTZ
|
||||||
|
)
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_role VARCHAR;
|
||||||
|
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;
|
||||||
|
|
||||||
|
v_role := v3.user_role_code(p_user_id);
|
||||||
|
IF v_role IS NULL THEN
|
||||||
|
RETURN; -- неизвестный/неактивный юзер → пустая маска
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ADMIN: UNION всех column_keys всех фаз отчёта.
|
||||||
|
IF v_role = 'ADMIN' THEN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT ck AS column_key,
|
||||||
|
MAX(fp.closes_at) AS closes_at
|
||||||
|
FROM v3.form3_phase fp,
|
||||||
|
LATERAL unnest(fp.column_keys) AS ck
|
||||||
|
WHERE fp.rf_project_report_id = p_report_id
|
||||||
|
GROUP BY ck;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- EXECUTOR_RF без назначения → пустая маска (default deny).
|
||||||
|
IF v_role = 'EXECUTOR_RF' AND NOT v3.user_in_report_org(p_user_id, p_report_id) THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT ck AS column_key,
|
||||||
|
MAX(fp.closes_at) AS closes_at
|
||||||
|
FROM v3.form3_phase fp,
|
||||||
|
LATERAL unnest(fp.column_keys) AS ck
|
||||||
|
WHERE fp.rf_project_report_id = p_report_id
|
||||||
|
AND fp.role = v_role
|
||||||
|
AND now() BETWEEN fp.opens_at AND fp.closes_at
|
||||||
|
GROUP BY ck;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql STABLE;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.can_edit3(
|
||||||
|
p_report_id INT,
|
||||||
|
p_column_key TEXT,
|
||||||
|
p_user_id INT
|
||||||
|
)
|
||||||
|
RETURNS JSONB
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_role VARCHAR;
|
||||||
|
v_active_close TIMESTAMPTZ;
|
||||||
|
v_role_has_col BOOLEAN;
|
||||||
|
v_next_open TIMESTAMPTZ;
|
||||||
|
v_last_close TIMESTAMPTZ;
|
||||||
|
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;
|
||||||
|
|
||||||
|
v_role := v3.user_role_code(p_user_id);
|
||||||
|
IF v_role IS NULL THEN
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'ok', false,
|
||||||
|
'code', 'role_not_allowed',
|
||||||
|
'detail', 'unknown or inactive user'
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ADMIN — bypass всех проверок.
|
||||||
|
IF v_role = 'ADMIN' THEN
|
||||||
|
RETURN jsonb_build_object('ok', true, 'admin', true);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- EXECUTOR_RF: фильтр по user_org проекта отчёта.
|
||||||
|
IF v_role = 'EXECUTOR_RF' AND NOT v3.user_in_report_org(p_user_id, p_report_id) THEN
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'ok', false,
|
||||||
|
'code', 'org_not_assigned',
|
||||||
|
'detail', p_column_key
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 1. Активная сейчас фаза, в которой эта колонка перечислена для этой роли?
|
||||||
|
SELECT MAX(fp.closes_at)
|
||||||
|
INTO v_active_close
|
||||||
|
FROM v3.form3_phase fp
|
||||||
|
WHERE fp.rf_project_report_id = p_report_id
|
||||||
|
AND fp.role = v_role
|
||||||
|
AND p_column_key = ANY(fp.column_keys)
|
||||||
|
AND now() BETWEEN fp.opens_at AND fp.closes_at;
|
||||||
|
|
||||||
|
IF v_active_close IS NOT NULL THEN
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'ok', true,
|
||||||
|
'closes_at', v_active_close
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 2. Колонка В ПРИНЦИПЕ есть хоть в одной фазе этой роли на этом отчёте?
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM v3.form3_phase fp
|
||||||
|
WHERE fp.rf_project_report_id = p_report_id
|
||||||
|
AND fp.role = v_role
|
||||||
|
AND p_column_key = ANY(fp.column_keys)
|
||||||
|
) INTO v_role_has_col;
|
||||||
|
|
||||||
|
IF NOT v_role_has_col THEN
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'ok', false,
|
||||||
|
'code', 'role_not_allowed',
|
||||||
|
'detail', p_column_key
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 3. Колонка доступна, но окно не активно. Подсказать ближайшее.
|
||||||
|
SELECT MIN(fp.opens_at)
|
||||||
|
INTO v_next_open
|
||||||
|
FROM v3.form3_phase fp
|
||||||
|
WHERE fp.rf_project_report_id = p_report_id
|
||||||
|
AND fp.role = v_role
|
||||||
|
AND p_column_key = ANY(fp.column_keys)
|
||||||
|
AND fp.opens_at > now();
|
||||||
|
|
||||||
|
IF v_next_open IS NOT NULL THEN
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'ok', false,
|
||||||
|
'code', 'window_closed',
|
||||||
|
'detail', p_column_key,
|
||||||
|
'opens_at', v_next_open
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT MAX(fp.closes_at)
|
||||||
|
INTO v_last_close
|
||||||
|
FROM v3.form3_phase fp
|
||||||
|
WHERE fp.rf_project_report_id = p_report_id
|
||||||
|
AND fp.role = v_role
|
||||||
|
AND p_column_key = ANY(fp.column_keys)
|
||||||
|
AND fp.closes_at <= now();
|
||||||
|
|
||||||
|
RETURN jsonb_build_object(
|
||||||
|
'ok', false,
|
||||||
|
'code', 'window_closed',
|
||||||
|
'detail', p_column_key,
|
||||||
|
'closes_at', v_last_close
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql STABLE;
|
||||||
|
|
||||||
|
-- ─── 3. Authz-обёртки upd_form3_cell/cells (доп. сигнатуры с p_user_id) ─────
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.upd_form3_cells(
|
||||||
|
p_report_id INT,
|
||||||
|
p_changes JSONB,
|
||||||
|
p_user_id INT
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
row_type VARCHAR,
|
||||||
|
depth INT,
|
||||||
|
sort_order BIGINT,
|
||||||
|
data JSONB
|
||||||
|
)
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_change JSONB;
|
||||||
|
v_column TEXT;
|
||||||
|
v_decision JSONB;
|
||||||
|
v_msg TEXT;
|
||||||
|
BEGIN
|
||||||
|
IF p_user_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'role_not_allowed: <missing user_id>';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.app_user WHERE id = p_user_id AND is_active) THEN
|
||||||
|
RAISE EXCEPTION 'role_not_allowed: unknown or inactive user %', p_user_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_changes IS NULL OR jsonb_typeof(p_changes) <> 'array' THEN
|
||||||
|
RAISE EXCEPTION 'p_changes must be a JSONB array';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes)
|
||||||
|
LOOP
|
||||||
|
v_column := v_change->>'column';
|
||||||
|
IF v_column IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'change must have column: %', v_change;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
v_decision := v3.can_edit3(p_report_id, v_column, p_user_id);
|
||||||
|
|
||||||
|
IF NOT (v_decision->>'ok')::boolean THEN
|
||||||
|
IF v_decision->>'code' = 'role_not_allowed' THEN
|
||||||
|
RAISE EXCEPTION 'role_not_allowed: %', v_column;
|
||||||
|
|
||||||
|
ELSIF v_decision->>'code' = 'org_not_assigned' THEN
|
||||||
|
RAISE EXCEPTION 'org_not_assigned: %', v_column;
|
||||||
|
|
||||||
|
ELSIF v_decision->>'code' = 'window_closed' THEN
|
||||||
|
IF v_decision ? 'opens_at' THEN
|
||||||
|
v_msg := format('window_closed: %s (opens %s)',
|
||||||
|
v_column, v_decision->>'opens_at');
|
||||||
|
ELSIF v_decision->>'closes_at' IS NOT NULL THEN
|
||||||
|
v_msg := format('window_closed: %s (closed %s)',
|
||||||
|
v_column, v_decision->>'closes_at');
|
||||||
|
ELSE
|
||||||
|
v_msg := format('window_closed: %s (no phase)', v_column);
|
||||||
|
END IF;
|
||||||
|
RAISE EXCEPTION '%', v_msg;
|
||||||
|
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION '%: %', v_decision->>'code', v_column;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT * FROM v3.upd_form3_cells(p_report_id, p_changes);
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql VOLATILE;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.upd_form3_cell(
|
||||||
|
p_report_id INT,
|
||||||
|
p_line_id INT,
|
||||||
|
p_column TEXT,
|
||||||
|
p_value JSONB,
|
||||||
|
p_user_id INT
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
row_type VARCHAR,
|
||||||
|
depth INT,
|
||||||
|
sort_order BIGINT,
|
||||||
|
data JSONB
|
||||||
|
)
|
||||||
|
AS $function$
|
||||||
|
SELECT * FROM v3.upd_form3_cells(
|
||||||
|
p_report_id,
|
||||||
|
jsonb_build_array(jsonb_build_object(
|
||||||
|
'line_id', p_line_id,
|
||||||
|
'column', p_column,
|
||||||
|
'value', p_value
|
||||||
|
)),
|
||||||
|
p_user_id
|
||||||
|
);
|
||||||
|
$function$
|
||||||
|
LANGUAGE sql VOLATILE;
|
||||||
|
|
||||||
|
-- ─── 4. add_project — раскладка snapshot обоих отчётов ──────────────────────
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.add_project(
|
||||||
|
p_name VARCHAR,
|
||||||
|
p_year INT,
|
||||||
|
p_org_unit_id INT,
|
||||||
|
p_level VARCHAR DEFAULT 'project',
|
||||||
|
p_parent_id INT DEFAULT NULL,
|
||||||
|
p_project_type VARCHAR DEFAULT NULL,
|
||||||
|
p_vsp_format VARCHAR DEFAULT NULL,
|
||||||
|
p_placement_type VARCHAR DEFAULT NULL,
|
||||||
|
p_object_address VARCHAR DEFAULT NULL,
|
||||||
|
p_staff_count INT DEFAULT NULL,
|
||||||
|
p_total_area NUMERIC DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
project_id INT,
|
||||||
|
limit_report_id INT,
|
||||||
|
current_expenses_report_id INT
|
||||||
|
)
|
||||||
|
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 INTO v3.project (
|
||||||
|
name, level, parent_id, org_unit_id,
|
||||||
|
project_type, vsp_format, placement_type, object_address, staff_count, total_area
|
||||||
|
) 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
|
||||||
|
) RETURNING id INTO v_pid;
|
||||||
|
|
||||||
|
INSERT INTO v3.rf_project_report (project_id, year, report_type)
|
||||||
|
VALUES (v_pid, p_year, 'LIMIT') RETURNING id INTO v_lim;
|
||||||
|
INSERT INTO v3.rf_project_report (project_id, year, report_type)
|
||||||
|
VALUES (v_pid, p_year, 'CURRENT_EXPENSES') RETURNING id INTO v_cur;
|
||||||
|
|
||||||
|
-- Snapshot RBAC-фаз (иначе default deny — отчёт read-only).
|
||||||
|
PERFORM v3.copy_template_to_report(v_lim);
|
||||||
|
PERFORM v3.copy_template_to_report(v_cur);
|
||||||
|
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'PROJECT_CREATE', 'PROJECT_CREATE',
|
||||||
|
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,
|
||||||
|
'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$
|
||||||
|
LANGUAGE plpgsql VOLATILE;
|
||||||
|
|
||||||
|
|||||||
@ -4250,4 +4250,29 @@ INSERT INTO v3.expense_item_form_type (expense_item_id,form_type_code) VALUES
|
|||||||
INSERT INTO v3.expense_item_form_type (expense_item_id,form_type_code) VALUES
|
INSERT INTO v3.expense_item_form_type (expense_item_id,form_type_code) VALUES
|
||||||
(798,'FORM_3');
|
(798,'FORM_3');
|
||||||
|
|
||||||
REFRESH MATERIALIZED VIEW CONCURRENTLY v3.mv_expense_item_tree;
|
REFRESH MATERIALIZED VIEW CONCURRENTLY v3.mv_expense_item_tree;
|
||||||
|
|
||||||
|
|
||||||
|
-- ─── 5. Seed фаз FORM_3 (идемпотентно, только FORM_3) ──────────────────────
|
||||||
|
-- DELETE скоупится на FORM_3 → фазы FORM_1/2/4 не затрагиваются.
|
||||||
|
DELETE FROM v3.phase_template WHERE form_type = 'FORM_3';
|
||||||
|
|
||||||
|
INSERT INTO v3.phase_template
|
||||||
|
(form_type, sheet, phase_code, role, column_keys, opens_at, closes_at)
|
||||||
|
SELECT 'FORM_3', sh, 'RF_FILL_2026', 'EXECUTOR_RF',
|
||||||
|
ARRAY[
|
||||||
|
'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'
|
||||||
|
],
|
||||||
|
'2026-01-01 00:00:00+00'::timestamptz,
|
||||||
|
'2026-12-31 23:59:59+00'::timestamptz
|
||||||
|
FROM (VALUES ('LIMIT'), ('CURRENT_EXPENSES')) AS s(sh);
|
||||||
|
|
||||||
|
-- ─── 6. Backfill v3.form3_phase для существующих отчётов ────────────────────
|
||||||
|
-- ON CONFLICT DO NOTHING внутри copy_template_to_report → повторный прогон
|
||||||
|
-- ничего не дублирует.
|
||||||
|
SELECT id, v3.copy_template_to_report(id) AS phases_inserted
|
||||||
|
FROM v3.rf_project_report
|
||||||
|
ORDER BY id;
|
||||||
@ -14,6 +14,8 @@ 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.services.rf_project_report_service import RfProjectReportService
|
||||||
|
from src.services.rf_project_report_line_service import RfProjectReportLineService
|
||||||
from src.repository.user_repository import UserRepository
|
from src.repository.user_repository import UserRepository
|
||||||
from src.services.budget_line_service import BudgetLineService
|
from src.services.budget_line_service import BudgetLineService
|
||||||
from src.api.v1.deps import get_user_by_token
|
from src.api.v1.deps import get_user_by_token
|
||||||
@ -490,7 +492,8 @@ class FormEventProcess:
|
|||||||
class ProjectEventProcess:
|
class ProjectEventProcess:
|
||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
self.project_service: ProjectService = ProjectService(db)
|
self.project_service: ProjectService = ProjectService(db)
|
||||||
self.bf_service: BudgetFormService = BudgetFormService(db)
|
self.prl_service: RfProjectReportLineService = RfProjectReportLineService(db)
|
||||||
|
self.pr_service: RfProjectReportService = RfProjectReportService(db)
|
||||||
self.user_service: UserService = UserService(db)
|
self.user_service: UserService = UserService(db)
|
||||||
|
|
||||||
async def process(
|
async def process(
|
||||||
@ -573,11 +576,16 @@ class ProjectEventProcess:
|
|||||||
expense_item_id: Optional[int] = None
|
expense_item_id: Optional[int] = None
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
project = await self.project_service.get(project_id=project_id, user=user)
|
project_report = await self.pr_service.get(
|
||||||
if not project:
|
year=year,
|
||||||
|
report_type=report_type,
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
if not project_report:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await self.project_service.add_form3_line(
|
ids = await self.prl_service.get_ids(project_report_id=project_report.id)
|
||||||
|
result = await self.project_service.add_form3_line(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
year=year,
|
year=year,
|
||||||
report_type=report_type,
|
report_type=report_type,
|
||||||
@ -585,6 +593,16 @@ class ProjectEventProcess:
|
|||||||
user=user,
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
final_result = {
|
||||||
|
"data": result,
|
||||||
|
"new_line_id": None,
|
||||||
|
}
|
||||||
|
for el in result:
|
||||||
|
if el[3]["line_id"] and el[3]["line_id"] not in ids:
|
||||||
|
final_result["new_line_id"] = el[3]["line_id"]
|
||||||
|
return final_result
|
||||||
|
return final_result
|
||||||
|
|
||||||
async def __del_row(
|
async def __del_row(
|
||||||
self,
|
self,
|
||||||
event_data: dict,
|
event_data: dict,
|
||||||
@ -595,7 +613,7 @@ class ProjectEventProcess:
|
|||||||
) -> list[tuple]:
|
) -> list[tuple]:
|
||||||
"""
|
"""
|
||||||
data = {
|
data = {
|
||||||
"line_id": int,
|
"row_id": int,
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
project = await self.project_service.get(project_id=project_id, user=user)
|
project = await self.project_service.get(project_id=project_id, user=user)
|
||||||
@ -606,7 +624,7 @@ class ProjectEventProcess:
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
year=year,
|
year=year,
|
||||||
report_type=report_type,
|
report_type=report_type,
|
||||||
line_id=event_data["line_id"],
|
line_id=event_data["row_id"],
|
||||||
user=user,
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -776,6 +794,7 @@ async def process_websocket(
|
|||||||
async with get_db_session(user_id=user_id) as db:
|
async with get_db_session(user_id=user_id) as db:
|
||||||
processor = processor_cls(db)
|
processor = processor_cls(db)
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
|
data_old = data.copy()
|
||||||
data["result"] = await processor.process(
|
data["result"] = await processor.process(
|
||||||
event_data=data,
|
event_data=data,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
@ -789,7 +808,7 @@ async def process_websocket(
|
|||||||
"time_ms": f"{db_ms:.2f}",
|
"time_ms": f"{db_ms:.2f}",
|
||||||
"event": data.get("event"),
|
"event": data.get("event"),
|
||||||
}
|
}
|
||||||
extra.update(data)
|
extra.update(data_old)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"db_time: {extra}",
|
f"db_time: {extra}",
|
||||||
extra=extra,
|
extra=extra,
|
||||||
|
|||||||
@ -10,6 +10,7 @@ from src.db.models.collegial_approval import CollegialApproval
|
|||||||
from src.db.models.contract_detail import ContractDetail
|
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
|
from src.db.models.expense_item import ExpenseItem
|
||||||
|
from src.db.models.form3_phase import Form3Phase
|
||||||
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
|
||||||
|
|||||||
27
api/src/db/models/form3_phase.py
Normal file
27
api/src/db/models/form3_phase.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import ARRAY, CheckConstraint, DateTime, ForeignKey, Index, Integer, String, Text, text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from src.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Form3Phase(Base):
|
||||||
|
__tablename__ = "form3_phase"
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint(text("cardinality(column_keys) > 0"), name="chk_v3_form3_phase_columns"),
|
||||||
|
CheckConstraint(text("role != 'ADMIN'"), name="chk_v3_form3_phase_no_admin"),
|
||||||
|
CheckConstraint(text("closes_at > opens_at"), name="chk_v3_form3_phase_window"),
|
||||||
|
Index("ix_v3_form3_phase_active", "rf_project_report_id", "role", "opens_at", "closes_at"),
|
||||||
|
Index("ix_v3_form3_phase_columns_gin", "column_keys", postgresql_using="gin"),
|
||||||
|
{"schema": "v3"},
|
||||||
|
)
|
||||||
|
|
||||||
|
rf_project_report_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("v3.rf_project_report.id", ondelete="CASCADE"), primary_key=True
|
||||||
|
)
|
||||||
|
phase_code: Mapped[str] = mapped_column(String, primary_key=True)
|
||||||
|
role: Mapped[str] = mapped_column(String, ForeignKey("v3.role.code"))
|
||||||
|
column_keys: Mapped[list[str]] = mapped_column(ARRAY(Text))
|
||||||
|
opens_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||||
|
closes_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||||
@ -362,7 +362,7 @@ class AddProjectBody(BaseModel):
|
|||||||
max_length=30,
|
max_length=30,
|
||||||
pattern=r"^[^+\-\/\\=&*\s]{1,30}$",
|
pattern=r"^[^+\-\/\\=&*\s]{1,30}$",
|
||||||
)
|
)
|
||||||
year: int
|
year: int = Field(default_factory=lambda: datetime.now().year)
|
||||||
branch_id: int
|
branch_id: int
|
||||||
level: Literal["project", "program"] = "project"
|
level: Literal["project", "program"] = "project"
|
||||||
parent_id: Optional[int] = None
|
parent_id: Optional[int] = None
|
||||||
|
|||||||
@ -12,7 +12,7 @@ class BudgetLineRepository:
|
|||||||
query = select(BudgetLine).where(BudgetLine.id == budget_line_id).limit(1)
|
query = select(BudgetLine).where(BudgetLine.id == budget_line_id).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, budget_line_ids: list[int]) -> BudgetLine | None:
|
async def get_list(self, budget_line_ids: list[int]) -> list[BudgetLine]:
|
||||||
query = select(BudgetLine).where(BudgetLine.id.in_(budget_line_ids))
|
query = select(BudgetLine).where(BudgetLine.id.in_(budget_line_ids))
|
||||||
return (await self.db.execute(query)).scalars().all()
|
return (await self.db.execute(query)).scalars().all()
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ class ProjectRepository:
|
|||||||
self.db = db
|
self.db = db
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _serialize_project(project: Project, org_unit_name: str | None, report_count: int) -> dict:
|
def _serialize_project(project: Project, org_unit_name: str | None, report_count: int, years: list[InterruptedError] | None = None) -> dict:
|
||||||
return {
|
return {
|
||||||
"id": project.id,
|
"id": project.id,
|
||||||
"name": project.name,
|
"name": project.name,
|
||||||
@ -29,6 +29,7 @@ class ProjectRepository:
|
|||||||
"org_unit_id": project.org_unit_id,
|
"org_unit_id": project.org_unit_id,
|
||||||
"org_unit_name": org_unit_name,
|
"org_unit_name": org_unit_name,
|
||||||
"report_count": int(report_count),
|
"report_count": int(report_count),
|
||||||
|
"years": years,
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -66,11 +67,19 @@ class ProjectRepository:
|
|||||||
.scalar_subquery()
|
.scalar_subquery()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
report_years_subq = (
|
||||||
|
select(RfProjectReport.year)
|
||||||
|
.where(RfProjectReport.project_id == Project.id)
|
||||||
|
.distinct()
|
||||||
|
.scalar_subquery()
|
||||||
|
)
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
select(
|
select(
|
||||||
Project,
|
Project,
|
||||||
OrgUnit.title.label("org_unit_name"),
|
OrgUnit.title.label("org_unit_name"),
|
||||||
report_count_subq.label("report_count"),
|
report_count_subq.label("report_count"),
|
||||||
|
func.array(report_years_subq).label("years"),
|
||||||
)
|
)
|
||||||
.outerjoin(OrgUnit, OrgUnit.id == Project.org_unit_id)
|
.outerjoin(OrgUnit, OrgUnit.id == Project.org_unit_id)
|
||||||
.order_by(Project.id)
|
.order_by(Project.id)
|
||||||
@ -82,7 +91,7 @@ class ProjectRepository:
|
|||||||
query = query.limit(limit)
|
query = query.limit(limit)
|
||||||
|
|
||||||
rows = (await self.db.execute(query)).all()
|
rows = (await self.db.execute(query)).all()
|
||||||
payload = [self._serialize_project(row[0], row[1], row[2]) for row in rows]
|
payload = [self._serialize_project(row[0], row[1], row[2], row[3]) for row in rows]
|
||||||
if not with_count:
|
if not with_count:
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
@ -101,11 +110,18 @@ class ProjectRepository:
|
|||||||
.where(RfProjectReport.project_id == Project.id)
|
.where(RfProjectReport.project_id == Project.id)
|
||||||
.scalar_subquery()
|
.scalar_subquery()
|
||||||
)
|
)
|
||||||
|
report_years_subq = (
|
||||||
|
select(RfProjectReport.year)
|
||||||
|
.where(RfProjectReport.project_id == Project.id)
|
||||||
|
.distinct()
|
||||||
|
.scalar_subquery()
|
||||||
|
)
|
||||||
query = (
|
query = (
|
||||||
select(
|
select(
|
||||||
Project,
|
Project,
|
||||||
OrgUnit.title.label("org_unit_name"),
|
OrgUnit.title.label("org_unit_name"),
|
||||||
report_count_subq.label("report_count"),
|
report_count_subq.label("report_count"),
|
||||||
|
func.array(report_years_subq).label("years"),
|
||||||
)
|
)
|
||||||
.outerjoin(OrgUnit, OrgUnit.id == Project.org_unit_id)
|
.outerjoin(OrgUnit, OrgUnit.id == Project.org_unit_id)
|
||||||
.where(Project.id == project_id)
|
.where(Project.id == project_id)
|
||||||
@ -117,7 +133,7 @@ class ProjectRepository:
|
|||||||
row = (await self.db.execute(query)).first()
|
row = (await self.db.execute(query)).first()
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
return self._serialize_project(row[0], row[1], row[2])
|
return self._serialize_project(row[0], row[1], row[2], row[3])
|
||||||
|
|
||||||
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 = (
|
||||||
|
|||||||
39
api/src/repository/rf_project_report_line_repository.py
Normal file
39
api/src/repository/rf_project_report_line_repository.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.rf_project_report_line import RfProjectReportLine
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RfProjectReportLineRepository:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get(self, line_id: int) -> RfProjectReportLine | None:
|
||||||
|
query = select(
|
||||||
|
RfProjectReportLine
|
||||||
|
).where(
|
||||||
|
RfProjectReportLine.id == line_id
|
||||||
|
).limit(1)
|
||||||
|
return (await self.db.execute(query)).scalar_one_or_none()
|
||||||
|
|
||||||
|
async def get_list(self, line_ids: list[int]) -> list[RfProjectReportLine]:
|
||||||
|
query = select(
|
||||||
|
RfProjectReportLine
|
||||||
|
).where(
|
||||||
|
RfProjectReportLine.id.in_(line_ids)
|
||||||
|
)
|
||||||
|
return (await self.db.execute(query)).scalars().all()
|
||||||
|
|
||||||
|
async def get_ids(
|
||||||
|
self,
|
||||||
|
project_report_id: int,
|
||||||
|
) -> list[int]:
|
||||||
|
|
||||||
|
query = select(
|
||||||
|
RfProjectReportLine.id
|
||||||
|
).where(
|
||||||
|
RfProjectReportLine.rf_project_report_id == project_report_id
|
||||||
|
)
|
||||||
|
return (await self.db.execute(query)).scalars().all()
|
||||||
|
|
||||||
33
api/src/repository/rf_project_report_repository.py
Normal file
33
api/src/repository/rf_project_report_repository.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.rf_project_report import RfProjectReport
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RfProjectReportRepository:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self,
|
||||||
|
report_id: int | None = None,
|
||||||
|
year: int | None = None,
|
||||||
|
report_type: str | None = None,
|
||||||
|
project_id: int | None = None,
|
||||||
|
) -> RfProjectReport | None:
|
||||||
|
assert report_id is not None or all((el is not None for el in (year, report_type, project_id)))
|
||||||
|
query = select(
|
||||||
|
RfProjectReport
|
||||||
|
)
|
||||||
|
if report_id is not None:
|
||||||
|
query = query.where(RfProjectReport.id == report_id)
|
||||||
|
else:
|
||||||
|
query = query.where(
|
||||||
|
RfProjectReport.year == year,
|
||||||
|
RfProjectReport.report_type == report_type,
|
||||||
|
RfProjectReport.project_id == project_id
|
||||||
|
)
|
||||||
|
|
||||||
|
query = query.limit(1)
|
||||||
|
return (await self.db.execute(query)).scalar_one_or_none()
|
||||||
34
api/src/services/rf_project_report_line_service.py
Normal file
34
api/src/services/rf_project_report_line_service.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RfProjectReportLineService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
self.prl_repo = RfProjectReportLineRepository(db)
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self,
|
||||||
|
user: AppUser,
|
||||||
|
line_id: int,
|
||||||
|
) -> RfProjectReportLine | None:
|
||||||
|
return await self.prl_repo.get(line_id=line_id)
|
||||||
|
|
||||||
|
async def get_list(
|
||||||
|
self,
|
||||||
|
user: AppUser,
|
||||||
|
line_ids: list[int],
|
||||||
|
) -> list[RfProjectReportLine]:
|
||||||
|
return await self.prl_repo.get_list(line_ids=line_ids)
|
||||||
|
|
||||||
|
async def get_ids(
|
||||||
|
self,
|
||||||
|
project_report_id: int,
|
||||||
|
) -> list[int]:
|
||||||
|
return await self.prl_repo.get_ids(project_report_id=project_report_id)
|
||||||
31
api/src/services/rf_project_report_service.py
Normal file
31
api/src/services/rf_project_report_service.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.rf_project_report import RfProjectReport
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RfProjectReportService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
self.pr_repo = RfProjectReportRepository(db)
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self,
|
||||||
|
user: AppUser| None = None,
|
||||||
|
report_id: int | None = None,
|
||||||
|
year: int | None = None,
|
||||||
|
report_type: str | None = None,
|
||||||
|
project_id: int | None = None,
|
||||||
|
) -> RfProjectReport | None:
|
||||||
|
return await self.pr_repo.get(
|
||||||
|
report_id=report_id,
|
||||||
|
year=year,
|
||||||
|
report_type=report_type,
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
@ -26,7 +26,8 @@ SELECT setval('v3.budget_line_id_seq', 1);
|
|||||||
INSERT INTO v3.form_phase (budget_form_id,sheet,phase_code,"role",column_keys,opens_at,closes_at) VALUES
|
INSERT INTO v3.form_phase (budget_form_id,sheet,phase_code,"role",column_keys,opens_at,closes_at) VALUES
|
||||||
(1,'AHR','test','DFIP','{{plan.q1}}','2026-05-05 03:00:00+03','2026-06-06 03:00:00+03');
|
(1,'AHR','test','DFIP','{{plan.q1}}','2026-05-05 03:00:00+03','2026-06-06 03:00:00+03');
|
||||||
|
|
||||||
INSERT INTO v3.vsp (id,branch_id,reg_number,address,format,opened_at,placement_type,staff_count,total_area,closed_at,is_active,updated_at,is_deleted,created_at,created_by,system_code,updated_by,vsp_type,notes,location_form,rent_contract_num,rent_end_date) VALUES
|
INSERT INTO v3.vsp (id,branch_id,reg_number,address,format,opened_at,placement_type,staff_count,total_area,closed_at,is_active,updated_at,is_deleted,created_at,created_by,system_code,updated_by,vsp_type,notes,rent_contract_num,rent_end_date) VALUES
|
||||||
(1,2,'Тестовый всп 1','Тестовая 12','укукк','2026-05-07','ывс',2026,230,'2026-06-16',false,'2026-06-18 15:46:38.611903',false,'2026-06-04 14:36:59.737727',1,'1233',91,'','','встроенное помещение','',NULL),
|
(1,2,'Тестовый всп 1','Тестовая 12','укукк','2026-05-07','ывс',2026,230,'2026-06-16',false,'2026-06-18 15:46:38.611903',false,'2026-06-04 14:36:59.737727',1,'1233',91,'','','',NULL),
|
||||||
(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','2026-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','2026-10-02'),
|
||||||
SELECT setval('v3.vsp_id_seq', 2);
|
(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','2026-07-01');
|
||||||
|
SELECT setval('v3.vsp_id_seq', 3);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user