Compare commits
No commits in common. "0b451b24990e73cb6732f4a7fa0c97e2dcdfefbc" and "8c88acab4941b92031d08e53cc1119026b9d5d3f" have entirely different histories.
0b451b2499
...
8c88acab49
@ -762,43 +762,6 @@ 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(),
|
||||||
@ -957,6 +920,7 @@ def upgrade() -> None:
|
|||||||
sa.Column('vsp_type', sa.String(100)),
|
sa.Column('vsp_type', sa.String(100)),
|
||||||
sa.Column('notes', sa.Text()),
|
sa.Column('notes', sa.Text()),
|
||||||
sa.Column('location_form', sa.String()),
|
sa.Column('location_form', sa.String()),
|
||||||
|
sa.Column('numbers', sa.Integer()),
|
||||||
sa.Column('rent_contract_num', sa.String()),
|
sa.Column('rent_contract_num', sa.String()),
|
||||||
sa.Column('rent_end_date', sa.Date()),
|
sa.Column('rent_end_date', sa.Date()),
|
||||||
schema='v3',
|
schema='v3',
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
from alembic import op
|
|
||||||
|
|
||||||
|
|
||||||
revision = "0004"
|
|
||||||
down_revision = "0003"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.del_vsp(p_vsp_id integer, p_deleted_by integer)
|
|
||||||
RETURNS integer
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_old RECORD;
|
|
||||||
v_usr RECORD;
|
|
||||||
BEGIN
|
|
||||||
SELECT vsp.id, vsp.branch_id
|
|
||||||
INTO v_old
|
|
||||||
FROM v3.vsp vsp WHERE vsp.id = p_vsp_id;
|
|
||||||
IF NOT FOUND THEN
|
|
||||||
RAISE EXCEPTION 'vsp #% не существует', p_vsp_id;
|
|
||||||
END IF;
|
|
||||||
SELECT usr.id
|
|
||||||
INTO v_usr
|
|
||||||
FROM v3.app_user usr WHERE usr.id = p_deleted_by;
|
|
||||||
IF NOT FOUND THEN
|
|
||||||
RAISE EXCEPTION 'user #% не существует', p_deleted_by;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
UPDATE v3.vsp
|
|
||||||
SET is_deleted = true,
|
|
||||||
is_active = false,
|
|
||||||
updated_by = p_deleted_by
|
|
||||||
WHERE id = p_vsp_id;
|
|
||||||
UPDATE v3.vsp
|
|
||||||
SET closed_at = CURRENT_DATE
|
|
||||||
WHERE id = p_vsp_id
|
|
||||||
AND closed_at IS NULL;
|
|
||||||
|
|
||||||
PERFORM v3.log_event(
|
|
||||||
'DELETE_VSP', 'VSP',
|
|
||||||
jsonb_build_object(
|
|
||||||
'vsp_id', p_vsp_id,
|
|
||||||
'entity_id', p_vsp_id,
|
|
||||||
'branch_id', v_old.branch_id,
|
|
||||||
'updated_by', p_deleted_by
|
|
||||||
),
|
|
||||||
null, null, v_old.branch_id
|
|
||||||
);
|
|
||||||
|
|
||||||
RETURN p_vsp_id;
|
|
||||||
END;
|
|
||||||
$function$;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
pass
|
|
||||||
@ -1,330 +0,0 @@
|
|||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
revision = "0005"
|
|
||||||
down_revision = "0004"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.execute('ALTER TABLE v3.phase_template ALTER COLUMN opens_at TYPE timestamp USING opens_at::timestamp')
|
|
||||||
op.execute('ALTER TABLE v3.phase_template ALTER COLUMN closes_at TYPE timestamp USING closes_at::timestamp')
|
|
||||||
op.execute("ALTER TABLE v3.org_unit ADD COLUMN utc_offset VARCHAR(3) DEFAULT '+03' NOT NULL")
|
|
||||||
# заводим фазу с нужной таймзоной
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.copy_template_to_form(p_form_id integer)
|
|
||||||
RETURNS integer
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_form_type VARCHAR;
|
|
||||||
v_inserted INTEGER;
|
|
||||||
v_utc_offset VARCHAR(3);
|
|
||||||
v_org_unit INTEGER;
|
|
||||||
BEGIN
|
|
||||||
SELECT form_type_code, org_unit_id INTO v_form_type, v_org_unit
|
|
||||||
FROM v3.budget_form
|
|
||||||
WHERE id = p_form_id;
|
|
||||||
|
|
||||||
IF NOT FOUND THEN
|
|
||||||
RAISE EXCEPTION 'budget_form id=% не существует', p_form_id;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
SELECT utc_offset INTO v_utc_offset
|
|
||||||
FROM v3.org_unit
|
|
||||||
WHERE id = v_org_unit;
|
|
||||||
|
|
||||||
INSERT INTO v3.form_phase
|
|
||||||
(budget_form_id, sheet, phase_code, role, column_keys, opens_at, closes_at)
|
|
||||||
SELECT p_form_id, pt.sheet, pt.phase_code, pt.role,
|
|
||||||
pt.column_keys, (pt.opens_at::text || v_utc_offset)::timestamptz,
|
|
||||||
(pt.closes_at::text || v_utc_offset)::timestamptz
|
|
||||||
FROM v3.phase_template pt
|
|
||||||
WHERE pt.form_type = v_form_type
|
|
||||||
ON CONFLICT (budget_form_id, sheet, phase_code) DO NOTHING;
|
|
||||||
|
|
||||||
GET DIAGNOSTICS v_inserted = ROW_COUNT;
|
|
||||||
RETURN v_inserted;
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
# Создавая фазу добавляем таймзону орг юнита
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.add_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying, p_role character varying, p_column_keys text[], p_opens_at timestamp, p_closes_at timestamp)
|
|
||||||
RETURNS v3.form_phase
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_phase v3.form_phase;
|
|
||||||
v_org_unit_id INT;
|
|
||||||
v_org_unit_title VARCHAR;
|
|
||||||
v_form_type_code VARCHAR;
|
|
||||||
v_year INT;
|
|
||||||
v_task_name TEXT;
|
|
||||||
v_phase_id TEXT;
|
|
||||||
v_utc_offset VARCHAR(3);
|
|
||||||
BEGIN
|
|
||||||
SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year
|
|
||||||
INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year
|
|
||||||
FROM v3.budget_form bf
|
|
||||||
LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
|
|
||||||
WHERE bf.id = p_budget_form_id;
|
|
||||||
|
|
||||||
SELECT utc_offset INTO v_utc_offset
|
|
||||||
FROM v3.org_unit
|
|
||||||
WHERE id = v_org_unit_id;
|
|
||||||
|
|
||||||
INSERT INTO v3.form_phase(
|
|
||||||
budget_form_id, sheet, phase_code, role, column_keys, opens_at, closes_at
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
p_budget_form_id,
|
|
||||||
p_sheet,
|
|
||||||
p_phase_code,
|
|
||||||
p_role,
|
|
||||||
p_column_keys,
|
|
||||||
CASE WHEN p_opens_at IS NOT NULL THEN (p_opens_at::text || v_utc_offset)::timestamptz ELSE NULL END,
|
|
||||||
CASE WHEN p_closes_at IS NOT NULL THEN (p_closes_at::text || v_utc_offset)::timestamptz ELSE NULL END
|
|
||||||
|
|
||||||
)
|
|
||||||
RETURNING * INTO v_phase;
|
|
||||||
|
|
||||||
|
|
||||||
v_task_name := trim(concat(
|
|
||||||
COALESCE(v_form_type_code, ''),
|
|
||||||
CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
|
|
||||||
CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
|
|
||||||
));
|
|
||||||
v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code);
|
|
||||||
|
|
||||||
PERFORM v3.log_event(
|
|
||||||
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
|
||||||
jsonb_build_object(
|
|
||||||
'entity_type', 'form_phase',
|
|
||||||
'entity_id', p_budget_form_id,
|
|
||||||
'action', 'create',
|
|
||||||
'phase_id', v_phase_id,
|
|
||||||
'phase_name', p_phase_code,
|
|
||||||
'task_id', p_budget_form_id,
|
|
||||||
'form_id', p_budget_form_id,
|
|
||||||
'budget_form_id', p_budget_form_id,
|
|
||||||
'org_unit_id', v_org_unit_id,
|
|
||||||
'org_unit_name', v_org_unit_title,
|
|
||||||
'sheet', p_sheet,
|
|
||||||
'phase_code', p_phase_code,
|
|
||||||
'role', v_phase.role,
|
|
||||||
'column_keys', v_phase.column_keys,
|
|
||||||
'opens_at', v_phase.opens_at,
|
|
||||||
'closes_at', v_phase.closes_at
|
|
||||||
),
|
|
||||||
p_budget_form_id, null, v_org_unit_id
|
|
||||||
);
|
|
||||||
|
|
||||||
RETURN v_phase;
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
# Обнволяя фазу добавляем таймзону орг юнита
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.upd_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying, p_role character varying DEFAULT NULL::character varying, p_column_keys text[] DEFAULT NULL::text[], p_opens_at timestamp without time zone DEFAULT NULL::timestamp without time zone, p_closes_at timestamp without time zone DEFAULT NULL::timestamp without time zone)
|
|
||||||
RETURNS v3.form_phase
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_old v3.form_phase;
|
|
||||||
v_new v3.form_phase;
|
|
||||||
v_is_extend BOOLEAN := FALSE;
|
|
||||||
v_has_non_extend_change BOOLEAN := FALSE;
|
|
||||||
v_has_window_change BOOLEAN := FALSE;
|
|
||||||
v_changes JSONB := '{}'::jsonb;
|
|
||||||
v_org_unit_id INT;
|
|
||||||
v_org_unit_title VARCHAR;
|
|
||||||
v_form_type_code VARCHAR;
|
|
||||||
v_year INT;
|
|
||||||
v_task_name TEXT;
|
|
||||||
v_phase_id TEXT;
|
|
||||||
v_utc_offset VARCHAR(3);
|
|
||||||
v_opens_at timestamptz;
|
|
||||||
v_closes_at timestamptz;
|
|
||||||
BEGIN
|
|
||||||
SELECT * INTO v_old
|
|
||||||
FROM v3.form_phase
|
|
||||||
WHERE budget_form_id = p_budget_form_id
|
|
||||||
AND sheet = p_sheet
|
|
||||||
AND phase_code = p_phase_code;
|
|
||||||
|
|
||||||
IF NOT FOUND THEN
|
|
||||||
RAISE EXCEPTION 'form_phase не найден (form_id=%, sheet=%, phase=%)',
|
|
||||||
p_budget_form_id, p_sheet, p_phase_code;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year, ou.utc_offset
|
|
||||||
INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year, v_utc_offset
|
|
||||||
FROM v3.budget_form bf
|
|
||||||
LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
|
|
||||||
WHERE bf.id = p_budget_form_id;
|
|
||||||
v_task_name := trim(concat(
|
|
||||||
COALESCE(v_form_type_code, ''),
|
|
||||||
CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
|
|
||||||
CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
|
|
||||||
));
|
|
||||||
|
|
||||||
IF p_opens_at is not null THEN
|
|
||||||
v_opens_at = (p_opens_at::text || v_utc_offset)::timestamptz;
|
|
||||||
END IF;
|
|
||||||
IF p_closes_at is not null THEN
|
|
||||||
v_closes_at = (p_closes_at::text || v_utc_offset)::timestamptz;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
UPDATE v3.form_phase
|
|
||||||
SET role = COALESCE(p_role, role),
|
|
||||||
column_keys = COALESCE(p_column_keys, column_keys),
|
|
||||||
opens_at = COALESCE(v_opens_at, opens_at),
|
|
||||||
closes_at = COALESCE(v_closes_at, closes_at)
|
|
||||||
WHERE budget_form_id = p_budget_form_id
|
|
||||||
AND sheet = p_sheet
|
|
||||||
AND phase_code = p_phase_code
|
|
||||||
RETURNING * INTO v_new;
|
|
||||||
|
|
||||||
|
|
||||||
v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code);
|
|
||||||
|
|
||||||
v_is_extend := v_new.closes_at > v_old.closes_at;
|
|
||||||
v_has_non_extend_change := v_old.role IS DISTINCT FROM v_new.role
|
|
||||||
OR v_old.column_keys IS DISTINCT FROM v_new.column_keys
|
|
||||||
OR v_old.opens_at IS DISTINCT FROM v_new.opens_at;
|
|
||||||
v_has_window_change := v_has_non_extend_change
|
|
||||||
OR (v_old.closes_at IS DISTINCT FROM v_new.closes_at AND NOT v_is_extend);
|
|
||||||
|
|
||||||
IF v_old.role IS DISTINCT FROM v_new.role THEN
|
|
||||||
v_changes := v_changes || jsonb_build_object(
|
|
||||||
'role', jsonb_build_object('before', v_old.role, 'after', v_new.role)
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
IF v_old.column_keys IS DISTINCT FROM v_new.column_keys THEN
|
|
||||||
v_changes := v_changes || jsonb_build_object(
|
|
||||||
'column_keys', jsonb_build_object('before', v_old.column_keys, 'after', v_new.column_keys)
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
IF v_old.opens_at IS DISTINCT FROM v_new.opens_at THEN
|
|
||||||
v_changes := v_changes || jsonb_build_object(
|
|
||||||
'opens_at', jsonb_build_object(
|
|
||||||
'before', v_old.opens_at,
|
|
||||||
'after', v_new.opens_at
|
|
||||||
)
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
IF v_old.closes_at IS DISTINCT FROM v_new.closes_at THEN
|
|
||||||
v_changes := v_changes || jsonb_build_object(
|
|
||||||
'closes_at', jsonb_build_object(
|
|
||||||
'before', v_old.closes_at,
|
|
||||||
'after', v_new.closes_at
|
|
||||||
)
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF v_is_extend THEN
|
|
||||||
PERFORM v3.log_event(
|
|
||||||
'ACCESS_EXTEND', 'ACCESS',
|
|
||||||
jsonb_build_object(
|
|
||||||
'phase_id', v_phase_id,
|
|
||||||
'phase_name', p_phase_code,
|
|
||||||
'task_id', p_budget_form_id,
|
|
||||||
'task_name', NULLIF(v_task_name, ''),
|
|
||||||
'form_id', p_budget_form_id,
|
|
||||||
'budget_form_id', p_budget_form_id,
|
|
||||||
'org_unit_id', v_org_unit_id,
|
|
||||||
'org_unit_name', v_org_unit_title,
|
|
||||||
'sheet', p_sheet,
|
|
||||||
'phase_code', p_phase_code,
|
|
||||||
'closes_at_before', v_old.closes_at,
|
|
||||||
'closes_at_after', v_new.closes_at
|
|
||||||
),
|
|
||||||
p_budget_form_id
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF v_has_window_change THEN
|
|
||||||
PERFORM v3.log_event(
|
|
||||||
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
|
||||||
jsonb_build_object(
|
|
||||||
'entity_type', 'form_phase',
|
|
||||||
'entity_id', p_budget_form_id,
|
|
||||||
'action', 'update',
|
|
||||||
'phase_id', v_phase_id,
|
|
||||||
'phase_name', p_phase_code,
|
|
||||||
'task_id', p_budget_form_id,
|
|
||||||
'task_name', NULLIF(v_task_name, ''),
|
|
||||||
'form_id', p_budget_form_id,
|
|
||||||
'budget_form_id', p_budget_form_id,
|
|
||||||
'org_unit_id', v_org_unit_id,
|
|
||||||
'org_unit_name', v_org_unit_title,
|
|
||||||
'sheet', p_sheet,
|
|
||||||
'phase_code', p_phase_code,
|
|
||||||
'changes', v_changes
|
|
||||||
),
|
|
||||||
p_budget_form_id, null, v_org_unit_id
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RETURN v_new;
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
# при обновлении таймзоны орг юнита обновляем актуальные фазы, связанные с ними
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.upd_phases_timezones(p_org_unit_id integer, p_old_tz varchar(3), p_new_tz varchar(3))
|
|
||||||
RETURNS integer
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_result integer;
|
|
||||||
BEGIN
|
|
||||||
IF p_old_tz = p_new_tz THEN
|
|
||||||
RETURN 0;
|
|
||||||
END IF;
|
|
||||||
WITH updated AS (
|
|
||||||
UPDATE v3.form_phase
|
|
||||||
SET opens_at = ((opens_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz,
|
|
||||||
closes_at = ((closes_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz
|
|
||||||
WHERE budget_form_id IN (SELECT id FROM v3.budget_form WHERE org_unit_id = p_org_unit_id)
|
|
||||||
AND (
|
|
||||||
opens_at >= CURRENT_TIMESTAMP
|
|
||||||
OR closes_at >= CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
RETURNING *
|
|
||||||
)
|
|
||||||
SELECT COUNT(*) INTO v_result FROM updated;
|
|
||||||
RETURN v_result;
|
|
||||||
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
# op.add_column(
|
|
||||||
# 'org_unit',
|
|
||||||
# sa.Column(
|
|
||||||
# 'utc_offset',
|
|
||||||
# sa.String(3),
|
|
||||||
# nullable=False,
|
|
||||||
# server_default='+03',
|
|
||||||
# )
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
pass
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
import os
|
|
||||||
import re
|
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
|
|
||||||
revision = "0006"
|
|
||||||
down_revision = "0005"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
_DOLLAR_TAG_RE = re.compile(r"\$\w+\$")
|
|
||||||
|
|
||||||
|
|
||||||
def _find_dollar_tag(line: str) -> Optional[str]:
|
|
||||||
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: Optional[str] = None
|
|
||||||
|
|
||||||
for line in sql.split("\n"):
|
|
||||||
stripped = line.strip()
|
|
||||||
|
|
||||||
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", "0006_form3.sql")
|
|
||||||
with open(ddl_path) as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
statements = _split_statements(content)
|
|
||||||
for stmt in statements:
|
|
||||||
stripped = stmt.strip().rstrip(";").strip()
|
|
||||||
if not stripped:
|
|
||||||
continue
|
|
||||||
if all(l.strip().startswith("--") or not l.strip() for l in stripped.split("\n")):
|
|
||||||
continue
|
|
||||||
op.execute(stripped)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
pass
|
|
||||||
@ -1,180 +0,0 @@
|
|||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
revision = "0007"
|
|
||||||
down_revision = "0006"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.execute("""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.v_form1_smeta(p_year integer, p_org_unit_id integer)
|
|
||||||
RETURNS TABLE(row_type character varying, depth integer, section_code character varying, name character varying, supp_plan_q1 numeric, supp_plan_q2 numeric, supp_plan_q3 numeric, supp_plan_q4 numeric, supp_plan_year numeric, dev_plan_q1 numeric, dev_plan_q2 numeric, dev_plan_q3 numeric, dev_plan_q4 numeric, dev_plan_year numeric, total_plan_year numeric, supp_appr_q1 numeric, supp_appr_q2 numeric, supp_appr_q3 numeric, supp_appr_q4 numeric, supp_appr_year numeric, dev_appr_q1 numeric, dev_appr_q2 numeric, dev_appr_q3 numeric, dev_appr_q4 numeric, dev_appr_year numeric, total_appr_year numeric, supp_act_q1 numeric, supp_act_q2 numeric, supp_act_q3 numeric, supp_act_q4 numeric, supp_act_year numeric, dev_act_q1 numeric, dev_act_q2 numeric, dev_act_q3 numeric, dev_act_q4 numeric, dev_act_year numeric, total_act_year numeric, supp_corr_q2 numeric, supp_corr_q3 numeric, supp_corr_q4 numeric, dev_corr_q2 numeric, dev_corr_q3 numeric, dev_corr_q4 numeric)
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
STABLE
|
|
||||||
SET search_path TO 'v3', 'pg_catalog'
|
|
||||||
AS $function$
|
|
||||||
#variable_conflict use_column
|
|
||||||
DECLARE
|
|
||||||
v_ahr INT; v_cap INT; v_oper INT;
|
|
||||||
v_sections TEXT[] := ARRAY['plan','approved','q1','q2','q3','q4'];
|
|
||||||
BEGIN
|
|
||||||
-- Одна budget_form на лист содержит ОБА направления (budget_line.direction).
|
|
||||||
-- Support/Development различаются параметром p_direction, а не отдельной формой.
|
|
||||||
WITH ranked AS (
|
|
||||||
SELECT bf.id AS fid, ei.sheet AS sh
|
|
||||||
FROM budget_form bf
|
|
||||||
JOIN budget_line bl ON bl.budget_form_id = bf.id
|
|
||||||
JOIN expense_item ei ON ei.id = bl.expense_item_id
|
|
||||||
WHERE bf.year = p_year AND bf.org_unit_id = p_org_unit_id AND bf.form_type_code = 'FORM_1'
|
|
||||||
GROUP BY bf.id, ei.sheet
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
MAX(CASE WHEN sh='AHR' THEN fid END),
|
|
||||||
MAX(CASE WHEN sh='CAP' THEN fid END),
|
|
||||||
MAX(CASE WHEN sh='OPER' THEN fid END)
|
|
||||||
INTO v_ahr, v_cap, v_oper
|
|
||||||
FROM ranked;
|
|
||||||
|
|
||||||
RETURN QUERY
|
|
||||||
WITH
|
|
||||||
ahr_s AS (
|
|
||||||
SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type,
|
|
||||||
col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr,
|
|
||||||
col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr,
|
|
||||||
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
|
||||||
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
|
||||||
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
|
||||||
FROM v_form1_sheet_sections(v_ahr, 'AHR', 'Support', v_sections)
|
|
||||||
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
|
||||||
),
|
|
||||||
ahr_d AS (
|
|
||||||
SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type,
|
|
||||||
col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr,
|
|
||||||
col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr,
|
|
||||||
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
|
||||||
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
|
||||||
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
|
||||||
FROM v_form1_sheet_sections(v_ahr, 'AHR', 'Development', v_sections)
|
|
||||||
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
|
||||||
),
|
|
||||||
cap_s AS (
|
|
||||||
SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type,
|
|
||||||
col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr,
|
|
||||||
col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr,
|
|
||||||
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
|
||||||
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
|
||||||
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
|
||||||
FROM v_form1_sheet_sections(v_cap, 'CAP', 'Support', v_sections)
|
|
||||||
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
|
||||||
),
|
|
||||||
cap_d AS (
|
|
||||||
SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type,
|
|
||||||
col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr,
|
|
||||||
col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr,
|
|
||||||
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
|
||||||
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
|
||||||
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
|
||||||
FROM v_form1_sheet_sections(v_cap, 'CAP', 'Development', v_sections)
|
|
||||||
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
|
||||||
),
|
|
||||||
oper_s AS (
|
|
||||||
SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type,
|
|
||||||
col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr,
|
|
||||||
col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr,
|
|
||||||
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
|
||||||
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
|
||||||
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
|
||||||
FROM v_form1_sheet_sections(v_oper, 'OPER', 'Support', v_sections)
|
|
||||||
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
|
||||||
),
|
|
||||||
merged AS (
|
|
||||||
SELECT COALESCE(s.row_type, d.row_type) AS row_type,
|
|
||||||
COALESCE(s.depth, d.depth) AS depth,
|
|
||||||
COALESCE(s.sc, d.sc) AS sc,
|
|
||||||
COALESCE(s.nm, d.nm) AS nm,
|
|
||||||
s.pq1, s.pq2, s.pq3, s.pq4, s.pyr,
|
|
||||||
d.pq1 AS d_pq1, d.pq2 AS d_pq2, d.pq3 AS d_pq3, d.pq4 AS d_pq4, d.pyr AS d_pyr,
|
|
||||||
s.aq1, s.aq2, s.aq3, s.aq4, s.ayr,
|
|
||||||
d.aq1 AS d_aq1, d.aq2 AS d_aq2, d.aq3 AS d_aq3, d.aq4 AS d_aq4, d.ayr AS d_ayr,
|
|
||||||
s.fq1, s.fq2, s.fq3, s.fq4,
|
|
||||||
d.fq1 AS d_fq1, d.fq2 AS d_fq2, d.fq3 AS d_fq3, d.fq4 AS d_fq4,
|
|
||||||
s.np2, s.np3, s.np4,
|
|
||||||
d.np2 AS d_np2, d.np3 AS d_np3, d.np4 AS d_np4
|
|
||||||
FROM ahr_s s FULL OUTER JOIN ahr_d d ON s.sc = d.sc
|
|
||||||
UNION ALL
|
|
||||||
SELECT COALESCE(s.row_type, d.row_type), COALESCE(s.depth, d.depth),
|
|
||||||
COALESCE(s.sc, d.sc), COALESCE(s.nm, d.nm),
|
|
||||||
s.pq1, s.pq2, s.pq3, s.pq4, s.pyr,
|
|
||||||
d.pq1, d.pq2, d.pq3, d.pq4, d.pyr,
|
|
||||||
s.aq1, s.aq2, s.aq3, s.aq4, s.ayr,
|
|
||||||
d.aq1, d.aq2, d.aq3, d.aq4, d.ayr,
|
|
||||||
s.fq1, s.fq2, s.fq3, s.fq4,
|
|
||||||
d.fq1, d.fq2, d.fq3, d.fq4,
|
|
||||||
s.np2, s.np3, s.np4,
|
|
||||||
d.np2, d.np3, d.np4
|
|
||||||
FROM cap_s s FULL OUTER JOIN cap_d d ON s.sc = d.sc
|
|
||||||
UNION ALL
|
|
||||||
SELECT s.row_type, s.depth, s.sc, s.nm,
|
|
||||||
s.pq1, s.pq2, s.pq3, s.pq4, s.pyr,
|
|
||||||
NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC,
|
|
||||||
s.aq1, s.aq2, s.aq3, s.aq4, s.ayr,
|
|
||||||
NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC,
|
|
||||||
s.fq1, s.fq2, s.fq3, s.fq4,
|
|
||||||
NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC,
|
|
||||||
s.np2, s.np3, s.np4,
|
|
||||||
NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC
|
|
||||||
FROM oper_s s
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
m.row_type, m.depth, m.sc, m.nm,
|
|
||||||
m.pq1, m.pq2, m.pq3, m.pq4, m.pyr,
|
|
||||||
m.d_pq1, m.d_pq2, m.d_pq3, m.d_pq4, m.d_pyr,
|
|
||||||
COALESCE(m.pyr,0) + COALESCE(m.d_pyr,0) AS total_plan_year,
|
|
||||||
m.aq1, m.aq2, m.aq3, m.aq4, m.ayr,
|
|
||||||
m.d_aq1, m.d_aq2, m.d_aq3, m.d_aq4, m.d_ayr,
|
|
||||||
COALESCE(m.ayr,0) + COALESCE(m.d_ayr,0) AS total_appr_year,
|
|
||||||
m.fq1, m.fq2, m.fq3, m.fq4, COALESCE(m.fq1,0)+COALESCE(m.fq2,0)+COALESCE(m.fq3,0)+COALESCE(m.fq4,0),
|
|
||||||
m.d_fq1, m.d_fq2, m.d_fq3, m.d_fq4, COALESCE(m.d_fq1,0)+COALESCE(m.d_fq2,0)+COALESCE(m.d_fq3,0)+COALESCE(m.d_fq4,0),
|
|
||||||
COALESCE(m.fq1,0)+COALESCE(m.fq2,0)+COALESCE(m.fq3,0)+COALESCE(m.fq4,0)
|
|
||||||
+ COALESCE(m.d_fq1,0)+COALESCE(m.d_fq2,0)+COALESCE(m.d_fq3,0)+COALESCE(m.d_fq4,0),
|
|
||||||
m.np2, m.np3, m.np4,
|
|
||||||
m.d_np2, m.d_np3, m.d_np4
|
|
||||||
FROM merged m
|
|
||||||
ORDER BY m.sc;
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
""")
|
|
||||||
for t_name in (
|
|
||||||
"reserve", "contract_detail", "ckk", "allocation", "sequestration", "contract_summary",
|
|
||||||
"collegial_approval", "rent_detail", "utility_detail", "plan", "budget_line_quarter"
|
|
||||||
):
|
|
||||||
op.execute(f"""
|
|
||||||
delete from v3.{t_name} where line_id in (
|
|
||||||
select id from v3.budget_line where budget_form_id not in (select min(fid) from (
|
|
||||||
SELECT bf.id AS fid, bf.year as yr, bf.form_type_code as ftc, bf.org_unit_id as oui
|
|
||||||
FROM v3.budget_form bf
|
|
||||||
) ranked
|
|
||||||
group by yr, ftc, oui));
|
|
||||||
""")
|
|
||||||
op.execute("""
|
|
||||||
delete from v3.budget_line where budget_form_id not in (select min(fid) from (
|
|
||||||
SELECT bf.id AS fid, bf.year as yr, bf.form_type_code as ftc, bf.org_unit_id as oui
|
|
||||||
FROM v3.budget_form bf
|
|
||||||
) ranked
|
|
||||||
group by yr, ftc, oui);
|
|
||||||
""")
|
|
||||||
op.execute("""
|
|
||||||
delete from v3.budget_form where id not in (select min(fid) from (
|
|
||||||
SELECT bf.id AS fid, bf.year as yr, bf.form_type_code as ftc, bf.org_unit_id as oui
|
|
||||||
FROM v3.budget_form bf
|
|
||||||
) ranked
|
|
||||||
group by yr, ftc, oui);
|
|
||||||
""")
|
|
||||||
op.execute("ALTER TABLE v3.budget_form ADD CONSTRAINT uq_ftype_org_year UNIQUE (form_type_code, org_unit_id, year)")
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
pass
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
revision = "0008"
|
|
||||||
down_revision = "0007"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.execute("update v3.vsp set rent_end_date = coalesce(closed_at, opened_at) where rent_end_date is not null and opened_at is not null and rent_end_date < opened_at")
|
|
||||||
op.execute("alter table v3.vsp add CONSTRAINT chk_rent_date CHECK (((rent_end_date IS NULL) OR (opened_at IS NULL) OR (rent_end_date >= opened_at)))")
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
pass
|
|
||||||
@ -1,785 +0,0 @@
|
|||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
revision = "0009"
|
|
||||||
down_revision = "0008"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.copy_template_to_report(p_report_id integer)
|
|
||||||
RETURNS integer
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_report_type VARCHAR;
|
|
||||||
v_inserted INTEGER;
|
|
||||||
v_org_unit INTEGER;
|
|
||||||
v_utc_offset VARCHAR(3);
|
|
||||||
BEGIN
|
|
||||||
SELECT report_type, org_unit_id INTO v_report_type, v_org_unit
|
|
||||||
FROM v3.rf_project_report AS rpr
|
|
||||||
INNER JOIN v3.project AS p ON p.id = rpr.project_id
|
|
||||||
WHERE rpr.id = p_report_id;
|
|
||||||
|
|
||||||
IF NOT FOUND THEN
|
|
||||||
RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
SELECT utc_offset INTO v_utc_offset
|
|
||||||
FROM v3.org_unit
|
|
||||||
WHERE id = v_org_unit;
|
|
||||||
|
|
||||||
INSERT INTO v3.form3_phase
|
|
||||||
(rf_project_report_id, phase_code, role, column_keys, opens_at, closes_at)
|
|
||||||
SELECT p_report_id, pt.phase_code, pt.role,
|
|
||||||
pt.column_keys, (pt.opens_at::text || v_utc_offset)::timestamptz,
|
|
||||||
(pt.closes_at::text || v_utc_offset)::timestamptz
|
|
||||||
FROM v3.phase_template pt
|
|
||||||
WHERE pt.form_type = 'FORM_3'
|
|
||||||
AND pt.sheet = v_report_type
|
|
||||||
ON CONFLICT (rf_project_report_id, phase_code) DO NOTHING;
|
|
||||||
|
|
||||||
GET DIAGNOSTICS v_inserted = ROW_COUNT;
|
|
||||||
RETURN v_inserted;
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.add_form3_phase(p_rf_project_report_id integer, p_phase_code character varying, p_role character varying, p_column_keys text[], p_opens_at timestamp without time zone, p_closes_at timestamp without time zone)
|
|
||||||
RETURNS v3.form3_phase
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_phase v3.form3_phase;
|
|
||||||
v_org_unit_id INT;
|
|
||||||
v_org_unit_title VARCHAR;
|
|
||||||
v_year INT;
|
|
||||||
v_task_name TEXT;
|
|
||||||
v_phase_id TEXT;
|
|
||||||
v_utc_offset VARCHAR(3);
|
|
||||||
v_report_type VARCHAR;
|
|
||||||
v_project_id INT;
|
|
||||||
BEGIN
|
|
||||||
SELECT p.org_unit_id, ou.title, rpr.year, ou.utc_offset, rpr.report_type, rpr.project_id
|
|
||||||
INTO v_org_unit_id, v_org_unit_title, v_year, v_utc_offset, v_report_type, v_project_id
|
|
||||||
FROM v3.rf_project_report rpr
|
|
||||||
LEFT JOIN v3.project p ON p.id = rpr.project_id
|
|
||||||
LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id
|
|
||||||
WHERE rpr.id = p_rf_project_report_id;
|
|
||||||
|
|
||||||
INSERT INTO v3.form3_phase
|
|
||||||
(rf_project_report_id, phase_code, role, column_keys, opens_at, closes_at)
|
|
||||||
VALUES (
|
|
||||||
p_rf_project_report_id,
|
|
||||||
p_phase_code,
|
|
||||||
p_role,
|
|
||||||
p_column_keys,
|
|
||||||
CASE WHEN p_opens_at IS NOT NULL THEN (p_opens_at::text || v_utc_offset)::timestamptz ELSE NULL END,
|
|
||||||
CASE WHEN p_closes_at IS NOT NULL THEN (p_closes_at::text || v_utc_offset)::timestamptz ELSE NULL END
|
|
||||||
|
|
||||||
)
|
|
||||||
RETURNING * INTO v_phase;
|
|
||||||
|
|
||||||
|
|
||||||
v_task_name := trim(concat(
|
|
||||||
'FORM_3',
|
|
||||||
CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
|
|
||||||
CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
|
|
||||||
));
|
|
||||||
v_phase_id := format('%s:%s', p_rf_project_report_id, p_phase_code);
|
|
||||||
|
|
||||||
PERFORM v3.log_event(
|
|
||||||
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
|
||||||
jsonb_build_object(
|
|
||||||
'entity_type', 'project',
|
|
||||||
'entity_id', v_project_id,
|
|
||||||
'core_entity_type', 'form3_phase',
|
|
||||||
'core_entity_id', v_phase_id,
|
|
||||||
'action', 'create',
|
|
||||||
'phase_id', v_phase_id,
|
|
||||||
'task_name', NULLIF(v_task_name, ''),
|
|
||||||
'report_id', p_rf_project_report_id,
|
|
||||||
'org_unit_id', v_org_unit_id,
|
|
||||||
'org_unit_name', v_org_unit_title,
|
|
||||||
'year', v_year,
|
|
||||||
'project_id', v_project_id,
|
|
||||||
'report_type', v_report_type,
|
|
||||||
'phase_code', p_phase_code,
|
|
||||||
'role', v_phase.role,
|
|
||||||
'column_keys', v_phase.column_keys,
|
|
||||||
'opens_at', v_phase.opens_at,
|
|
||||||
'closes_at', v_phase.closes_at
|
|
||||||
),
|
|
||||||
null, null, v_org_unit_id
|
|
||||||
);
|
|
||||||
|
|
||||||
RETURN v_phase;
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.upd_form3_phase(p_rf_project_report_id integer, p_phase_code character varying, p_role character varying DEFAULT NULL::character varying, p_column_keys text[] DEFAULT NULL::text[], p_opens_at timestamp without time zone DEFAULT NULL::timestamp without time zone, p_closes_at timestamp without time zone DEFAULT NULL::timestamp without time zone)
|
|
||||||
RETURNS v3.form3_phase
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_old v3.form3_phase;
|
|
||||||
v_new v3.form3_phase;
|
|
||||||
v_is_extend BOOLEAN := FALSE;
|
|
||||||
v_has_non_extend_change BOOLEAN := FALSE;
|
|
||||||
v_has_window_change BOOLEAN := FALSE;
|
|
||||||
v_changes JSONB := '{}'::jsonb;
|
|
||||||
v_org_unit_id INT;
|
|
||||||
v_org_unit_title VARCHAR;
|
|
||||||
v_form_type_code VARCHAR;
|
|
||||||
v_year INT;
|
|
||||||
v_task_name TEXT;
|
|
||||||
v_phase_id TEXT;
|
|
||||||
v_utc_offset VARCHAR(3);
|
|
||||||
v_opens_at timestamptz;
|
|
||||||
v_closes_at timestamptz;
|
|
||||||
v_report_type VARCHAR;
|
|
||||||
v_project_id INT;
|
|
||||||
BEGIN
|
|
||||||
SELECT * INTO v_old
|
|
||||||
FROM v3.form3_phase
|
|
||||||
WHERE rf_project_report_id = p_rf_project_report_id
|
|
||||||
AND phase_code = p_phase_code;
|
|
||||||
|
|
||||||
IF NOT FOUND THEN
|
|
||||||
RAISE EXCEPTION 'form3_phase не найден (rf_project_report_id=%, phase=%)',
|
|
||||||
p_rf_project_report_id, p_phase_code;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
SELECT p.org_unit_id, ou.title, rpr.year, ou.utc_offset, rpr.report_type, rpr.project_id
|
|
||||||
INTO v_org_unit_id, v_org_unit_title, v_year, v_utc_offset, v_report_type, v_project_id
|
|
||||||
FROM v3.rf_project_report rpr
|
|
||||||
LEFT JOIN v3.project p ON p.id = rpr.project_id
|
|
||||||
LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id
|
|
||||||
WHERE rpr.id = p_rf_project_report_id;
|
|
||||||
v_task_name := trim(concat(
|
|
||||||
'FORM_3',
|
|
||||||
CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
|
|
||||||
CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
|
|
||||||
));
|
|
||||||
|
|
||||||
IF p_opens_at is not null THEN
|
|
||||||
v_opens_at = (p_opens_at::text || v_utc_offset)::timestamptz;
|
|
||||||
END IF;
|
|
||||||
IF p_closes_at is not null THEN
|
|
||||||
v_closes_at = (p_closes_at::text || v_utc_offset)::timestamptz;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
UPDATE v3.form3_phase
|
|
||||||
SET role = COALESCE(p_role, role),
|
|
||||||
column_keys = COALESCE(p_column_keys, column_keys),
|
|
||||||
opens_at = COALESCE(v_opens_at, opens_at),
|
|
||||||
closes_at = COALESCE(v_closes_at, closes_at)
|
|
||||||
WHERE rf_project_report_id = p_rf_project_report_id
|
|
||||||
AND phase_code = p_phase_code
|
|
||||||
RETURNING * INTO v_new;
|
|
||||||
|
|
||||||
|
|
||||||
v_phase_id := format('%s:%s', p_rf_project_report_id, p_phase_code);
|
|
||||||
|
|
||||||
v_is_extend := v_new.closes_at > v_old.closes_at;
|
|
||||||
v_has_non_extend_change := v_old.role IS DISTINCT FROM v_new.role
|
|
||||||
OR v_old.column_keys IS DISTINCT FROM v_new.column_keys
|
|
||||||
OR v_old.opens_at IS DISTINCT FROM v_new.opens_at;
|
|
||||||
v_has_window_change := v_has_non_extend_change
|
|
||||||
OR (v_old.closes_at IS DISTINCT FROM v_new.closes_at AND NOT v_is_extend);
|
|
||||||
|
|
||||||
IF v_old.role IS DISTINCT FROM v_new.role THEN
|
|
||||||
v_changes := v_changes || jsonb_build_object(
|
|
||||||
'role', jsonb_build_object('before', v_old.role, 'after', v_new.role)
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
IF v_old.column_keys IS DISTINCT FROM v_new.column_keys THEN
|
|
||||||
v_changes := v_changes || jsonb_build_object(
|
|
||||||
'column_keys', jsonb_build_object('before', v_old.column_keys, 'after', v_new.column_keys)
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
IF v_old.opens_at IS DISTINCT FROM v_new.opens_at THEN
|
|
||||||
v_changes := v_changes || jsonb_build_object(
|
|
||||||
'opens_at', jsonb_build_object(
|
|
||||||
'before', v_old.opens_at,
|
|
||||||
'after', v_new.opens_at
|
|
||||||
)
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
IF v_old.closes_at IS DISTINCT FROM v_new.closes_at THEN
|
|
||||||
v_changes := v_changes || jsonb_build_object(
|
|
||||||
'closes_at', jsonb_build_object(
|
|
||||||
'before', v_old.closes_at,
|
|
||||||
'after', v_new.closes_at
|
|
||||||
)
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF v_is_extend THEN
|
|
||||||
PERFORM v3.log_event(
|
|
||||||
'ACCESS_EXTEND', 'ACCESS',
|
|
||||||
jsonb_build_object(
|
|
||||||
'entity_type', 'project',
|
|
||||||
'entity_id', v_project_id,
|
|
||||||
'core_entity_type', 'form3_phase',
|
|
||||||
'core_entity_id', v_phase_id,
|
|
||||||
'action', 'update',
|
|
||||||
'phase_id', v_phase_id,
|
|
||||||
'phase_name', p_phase_code,
|
|
||||||
'task_name', NULLIF(v_task_name, ''),
|
|
||||||
'report_id', p_rf_project_report_id,
|
|
||||||
'report_type', v_report_type,
|
|
||||||
'project_id', v_project_id,
|
|
||||||
'org_unit_id', v_org_unit_id,
|
|
||||||
'org_unit_name', v_org_unit_title,
|
|
||||||
'year', v_year,
|
|
||||||
'phase_code', p_phase_code,
|
|
||||||
'closes_at_before', v_old.closes_at,
|
|
||||||
'closes_at_after', v_new.closes_at
|
|
||||||
),
|
|
||||||
null, null, v_org_unit_id
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF v_has_window_change THEN
|
|
||||||
PERFORM v3.log_event(
|
|
||||||
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
|
||||||
jsonb_build_object(
|
|
||||||
'entity_type', 'project',
|
|
||||||
'entity_id', v_project_id,
|
|
||||||
'core_entity_type', 'form3_phase',
|
|
||||||
'core_entity_id', v_phase_id,
|
|
||||||
'action', 'update',
|
|
||||||
'phase_id', v_phase_id,
|
|
||||||
'phase_name', p_phase_code,
|
|
||||||
'task_id', p_rf_project_report_id,
|
|
||||||
'task_name', NULLIF(v_task_name, ''),
|
|
||||||
'project_id', v_project_id,
|
|
||||||
'report_type', v_report_type,
|
|
||||||
'year', v_year,
|
|
||||||
'org_unit_id', v_org_unit_id,
|
|
||||||
'org_unit_name', v_org_unit_title,
|
|
||||||
'phase_code', p_phase_code,
|
|
||||||
'changes', v_changes
|
|
||||||
),
|
|
||||||
null, null, v_org_unit_id
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RETURN v_new;
|
|
||||||
END;
|
|
||||||
$function$;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.del_form3_phase(p_rf_project_report_id integer, p_phase_code character varying)
|
|
||||||
RETURNS boolean
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_old v3.form3_phase;
|
|
||||||
v_org_unit_id INT;
|
|
||||||
v_org_unit_title VARCHAR;
|
|
||||||
v_year INT;
|
|
||||||
v_task_name TEXT;
|
|
||||||
v_phase_id TEXT;
|
|
||||||
v_report_type VARCHAR;
|
|
||||||
v_project_id INT;
|
|
||||||
BEGIN
|
|
||||||
DELETE FROM v3.form3_phase
|
|
||||||
WHERE rf_project_report_id = p_rf_project_report_id
|
|
||||||
AND phase_code = p_phase_code
|
|
||||||
RETURNING * INTO v_old;
|
|
||||||
|
|
||||||
IF NOT FOUND THEN
|
|
||||||
RETURN FALSE;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
SELECT p.org_unit_id, ou.title, rpr.year, rpr.report_type, rpr.project_id
|
|
||||||
INTO v_org_unit_id, v_org_unit_title, v_year, v_report_type, v_project_id
|
|
||||||
FROM v3.rf_project_report rpr
|
|
||||||
LEFT JOIN v3.project p ON p.id = rpr.project_id
|
|
||||||
LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id
|
|
||||||
WHERE rpr.id = p_rf_project_report_id;
|
|
||||||
v_task_name := trim(concat(
|
|
||||||
'FORM_3',
|
|
||||||
CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
|
|
||||||
CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
|
|
||||||
));
|
|
||||||
v_phase_id := format('%s:%s', p_rf_project_report_id, p_phase_code);
|
|
||||||
|
|
||||||
PERFORM v3.log_event(
|
|
||||||
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
|
||||||
jsonb_build_object(
|
|
||||||
'entity_type', 'project',
|
|
||||||
'entity_id', v_project_id,
|
|
||||||
'core_entity_type', 'form3_phase',
|
|
||||||
'core_entity_id', v_phase_id,
|
|
||||||
'action', 'delete',
|
|
||||||
'phase_id', v_phase_id,
|
|
||||||
'phase_name', p_phase_code,
|
|
||||||
'task_name', NULLIF(v_task_name, ''),
|
|
||||||
'project_id', v_project_id,
|
|
||||||
'year', v_year,
|
|
||||||
'report_type', v_report_type,
|
|
||||||
'org_unit_id', v_org_unit_id,
|
|
||||||
'org_unit_name', v_org_unit_title,
|
|
||||||
'phase_code', p_phase_code,
|
|
||||||
'role', v_old.role,
|
|
||||||
'column_keys', v_old.column_keys,
|
|
||||||
'opens_at', v_old.opens_at,
|
|
||||||
'closes_at', v_old.closes_at
|
|
||||||
),
|
|
||||||
null, null, v_org_unit_id
|
|
||||||
);
|
|
||||||
RETURN TRUE;
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.add_project(p_name character varying, p_year integer, p_org_unit_id integer, p_level character varying DEFAULT 'project'::character varying, p_parent_id integer DEFAULT NULL::integer, p_project_type character varying DEFAULT NULL::character varying, p_vsp_format character varying DEFAULT NULL::character varying, p_placement_type character varying DEFAULT NULL::character varying, p_object_address character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_system_code character varying DEFAULT NULL::character varying)
|
|
||||||
RETURNS TABLE(project_id integer, limit_report_id integer, current_expenses_report_id integer)
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_pid INT;
|
|
||||||
v_lim INT;
|
|
||||||
v_cur INT;
|
|
||||||
BEGIN
|
|
||||||
IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
|
|
||||||
RAISE EXCEPTION 'p_name обязателен';
|
|
||||||
END IF;
|
|
||||||
IF p_year IS NULL THEN
|
|
||||||
RAISE EXCEPTION 'p_year обязателен';
|
|
||||||
END IF;
|
|
||||||
IF p_org_unit_id IS NULL THEN
|
|
||||||
RAISE EXCEPTION 'p_org_unit_id обязателен';
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM v3.org_unit WHERE id = p_org_unit_id) THEN
|
|
||||||
RAISE EXCEPTION 'org_unit id=% не существует', p_org_unit_id;
|
|
||||||
END IF;
|
|
||||||
IF p_parent_id IS NOT NULL AND
|
|
||||||
NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_parent_id AND level = 'program') THEN
|
|
||||||
RAISE EXCEPTION 'parent project id=% не существует или не имеет level=program', p_parent_id;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- INSERT project (CHECK-ы name/project_type/vsp_format/placement_type
|
|
||||||
-- из migrate_form3_v1.sql применятся автоматически).
|
|
||||||
INSERT INTO v3.project (
|
|
||||||
name, level, parent_id, org_unit_id,
|
|
||||||
project_type, vsp_format, placement_type, object_address, staff_count, total_area,
|
|
||||||
system_code
|
|
||||||
) VALUES (
|
|
||||||
p_name, p_level, p_parent_id, p_org_unit_id,
|
|
||||||
p_project_type, p_vsp_format, p_placement_type, p_object_address, p_staff_count, p_total_area,
|
|
||||||
p_system_code
|
|
||||||
) RETURNING id INTO v_pid;
|
|
||||||
|
|
||||||
-- Первый год: 2 отчёта (LIMIT + CURRENT_EXPENSES) + snapshot фаз + сетка статей.
|
|
||||||
-- p_emit_audit=false — событие года покрыто PROJECT_CREATE ниже. Дальнейшие
|
|
||||||
-- годы добавляются отдельно через v3.add_project_year.
|
|
||||||
SELECT y.limit_report_id, y.current_expenses_report_id
|
|
||||||
INTO v_lim, v_cur
|
|
||||||
FROM v3.add_project_year(v_pid, p_year, false) y;
|
|
||||||
|
|
||||||
-- Аудит: PROJECT_CREATE — все заполненные поля + id отчётов
|
|
||||||
PERFORM v3.log_event(
|
|
||||||
'PROJECT_CREATE', 'PROJECT',
|
|
||||||
jsonb_strip_nulls(jsonb_build_object(
|
|
||||||
'project_id', v_pid,
|
|
||||||
'name', p_name,
|
|
||||||
'year', p_year,
|
|
||||||
'level', p_level,
|
|
||||||
'parent_id', p_parent_id,
|
|
||||||
'org_unit_id', p_org_unit_id,
|
|
||||||
'project_type', p_project_type,
|
|
||||||
'vsp_format', p_vsp_format,
|
|
||||||
'placement_type', p_placement_type,
|
|
||||||
'object_address', p_object_address,
|
|
||||||
'system_code', p_system_code,
|
|
||||||
'staff_count', p_staff_count,
|
|
||||||
'total_area', p_total_area,
|
|
||||||
'limit_report_id', v_lim,
|
|
||||||
'current_expenses_report_id', v_cur
|
|
||||||
))
|
|
||||||
);
|
|
||||||
|
|
||||||
RETURN QUERY SELECT v_pid, v_lim, v_cur;
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.upd_phases_timezones(p_org_unit_id integer, p_old_tz character varying, p_new_tz character varying)
|
|
||||||
RETURNS integer
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_result integer;
|
|
||||||
v_result3 integer;
|
|
||||||
BEGIN
|
|
||||||
IF p_old_tz = p_new_tz THEN
|
|
||||||
RETURN 0;
|
|
||||||
END IF;
|
|
||||||
WITH updated AS (
|
|
||||||
UPDATE v3.form_phase
|
|
||||||
SET opens_at = ((opens_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz,
|
|
||||||
closes_at = ((closes_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz
|
|
||||||
WHERE budget_form_id IN (SELECT id FROM v3.budget_form WHERE org_unit_id = p_org_unit_id)
|
|
||||||
AND (
|
|
||||||
opens_at >= CURRENT_TIMESTAMP
|
|
||||||
OR closes_at >= CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
RETURNING *
|
|
||||||
)
|
|
||||||
SELECT COUNT(*) INTO v_result FROM updated;
|
|
||||||
|
|
||||||
WITH updated AS (
|
|
||||||
UPDATE v3.form3_phase
|
|
||||||
SET opens_at = ((opens_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz,
|
|
||||||
closes_at = ((closes_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz
|
|
||||||
WHERE rf_project_report_id IN (
|
|
||||||
SELECT id FROM V3.rf_project_report rpr WHERE project_id IN (
|
|
||||||
SELECT id FROM v3.project WHERE org_unit_id = p_org_unit_id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
AND (
|
|
||||||
opens_at >= CURRENT_TIMESTAMP
|
|
||||||
OR closes_at >= CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
RETURNING *
|
|
||||||
)
|
|
||||||
SELECT COUNT(*) INTO v_result3 FROM updated;
|
|
||||||
RETURN v_result + v_result3;
|
|
||||||
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.add_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying, p_role character varying, p_column_keys text[], p_opens_at timestamp without time zone, p_closes_at timestamp without time zone)
|
|
||||||
RETURNS v3.form_phase
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_phase v3.form_phase;
|
|
||||||
v_org_unit_id INT;
|
|
||||||
v_org_unit_title VARCHAR;
|
|
||||||
v_form_type_code VARCHAR;
|
|
||||||
v_year INT;
|
|
||||||
v_task_name TEXT;
|
|
||||||
v_phase_id TEXT;
|
|
||||||
v_utc_offset VARCHAR(3);
|
|
||||||
BEGIN
|
|
||||||
SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year
|
|
||||||
INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year
|
|
||||||
FROM v3.budget_form bf
|
|
||||||
LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
|
|
||||||
WHERE bf.id = p_budget_form_id;
|
|
||||||
|
|
||||||
SELECT utc_offset INTO v_utc_offset
|
|
||||||
FROM v3.org_unit
|
|
||||||
WHERE id = v_org_unit_id;
|
|
||||||
|
|
||||||
INSERT INTO v3.form_phase(
|
|
||||||
budget_form_id, sheet, phase_code, role, column_keys, opens_at, closes_at
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
p_budget_form_id,
|
|
||||||
p_sheet,
|
|
||||||
p_phase_code,
|
|
||||||
p_role,
|
|
||||||
p_column_keys,
|
|
||||||
CASE WHEN p_opens_at IS NOT NULL THEN (p_opens_at::text || v_utc_offset)::timestamptz ELSE NULL END,
|
|
||||||
CASE WHEN p_closes_at IS NOT NULL THEN (p_closes_at::text || v_utc_offset)::timestamptz ELSE NULL END
|
|
||||||
|
|
||||||
)
|
|
||||||
RETURNING * INTO v_phase;
|
|
||||||
|
|
||||||
|
|
||||||
v_task_name := trim(concat(
|
|
||||||
COALESCE(v_form_type_code, ''),
|
|
||||||
CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
|
|
||||||
CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
|
|
||||||
));
|
|
||||||
v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code);
|
|
||||||
|
|
||||||
PERFORM v3.log_event(
|
|
||||||
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
|
||||||
jsonb_build_object(
|
|
||||||
'entity_type', 'task',
|
|
||||||
'entity_id', p_budget_form_id,
|
|
||||||
'core_entity_type', 'form_phase',
|
|
||||||
'core_entity_id', v_phase_id,
|
|
||||||
'action', 'create',
|
|
||||||
'phase_id', v_phase_id,
|
|
||||||
'phase_name', p_phase_code,
|
|
||||||
'task_id', p_budget_form_id,
|
|
||||||
'form_id', p_budget_form_id,
|
|
||||||
'budget_form_id', p_budget_form_id,
|
|
||||||
'org_unit_id', v_org_unit_id,
|
|
||||||
'org_unit_name', v_org_unit_title,
|
|
||||||
'sheet', p_sheet,
|
|
||||||
'phase_code', p_phase_code,
|
|
||||||
'role', v_phase.role,
|
|
||||||
'column_keys', v_phase.column_keys,
|
|
||||||
'opens_at', v_phase.opens_at,
|
|
||||||
'closes_at', v_phase.closes_at
|
|
||||||
),
|
|
||||||
p_budget_form_id, null, v_org_unit_id
|
|
||||||
);
|
|
||||||
|
|
||||||
RETURN v_phase;
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.del_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying)
|
|
||||||
RETURNS boolean
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_old v3.form_phase;
|
|
||||||
v_org_unit_id INT;
|
|
||||||
v_org_unit_title VARCHAR;
|
|
||||||
v_form_type_code VARCHAR;
|
|
||||||
v_year INT;
|
|
||||||
v_task_name TEXT;
|
|
||||||
v_phase_id TEXT;
|
|
||||||
BEGIN
|
|
||||||
DELETE FROM v3.form_phase
|
|
||||||
WHERE budget_form_id = p_budget_form_id
|
|
||||||
AND sheet = p_sheet
|
|
||||||
AND phase_code = p_phase_code
|
|
||||||
RETURNING * INTO v_old;
|
|
||||||
|
|
||||||
IF NOT FOUND THEN
|
|
||||||
RETURN FALSE;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year
|
|
||||||
INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year
|
|
||||||
FROM v3.budget_form bf
|
|
||||||
LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
|
|
||||||
WHERE bf.id = p_budget_form_id;
|
|
||||||
v_task_name := trim(concat(
|
|
||||||
COALESCE(v_form_type_code, ''),
|
|
||||||
CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
|
|
||||||
CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
|
|
||||||
));
|
|
||||||
v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code);
|
|
||||||
|
|
||||||
PERFORM v3.log_event(
|
|
||||||
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
|
||||||
jsonb_build_object(
|
|
||||||
'entity_type', 'task',
|
|
||||||
'entity_id', p_budget_form_id,
|
|
||||||
'core_entity_type', 'form_phase',
|
|
||||||
'core_entity_id', v_phase_id,
|
|
||||||
'action', 'delete',
|
|
||||||
'phase_id', v_phase_id,
|
|
||||||
'phase_name', p_phase_code,
|
|
||||||
'task_id', p_budget_form_id,
|
|
||||||
'task_name', NULLIF(v_task_name, ''),
|
|
||||||
'form_id', p_budget_form_id,
|
|
||||||
'budget_form_id', p_budget_form_id,
|
|
||||||
'org_unit_id', v_org_unit_id,
|
|
||||||
'org_unit_name', v_org_unit_title,
|
|
||||||
'sheet', p_sheet,
|
|
||||||
'phase_code', p_phase_code,
|
|
||||||
'role', v_old.role,
|
|
||||||
'column_keys', v_old.column_keys,
|
|
||||||
'opens_at', v_old.opens_at,
|
|
||||||
'closes_at', v_old.closes_at
|
|
||||||
),
|
|
||||||
p_budget_form_id, null, v_org_unit_id
|
|
||||||
);
|
|
||||||
RETURN TRUE;
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.upd_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying, p_role character varying DEFAULT NULL::character varying, p_column_keys text[] DEFAULT NULL::text[], p_opens_at timestamp without time zone DEFAULT NULL::timestamp without time zone, p_closes_at timestamp without time zone DEFAULT NULL::timestamp without time zone)
|
|
||||||
RETURNS v3.form_phase
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_old v3.form_phase;
|
|
||||||
v_new v3.form_phase;
|
|
||||||
v_is_extend BOOLEAN := FALSE;
|
|
||||||
v_has_non_extend_change BOOLEAN := FALSE;
|
|
||||||
v_has_window_change BOOLEAN := FALSE;
|
|
||||||
v_changes JSONB := '{}'::jsonb;
|
|
||||||
v_org_unit_id INT;
|
|
||||||
v_org_unit_title VARCHAR;
|
|
||||||
v_form_type_code VARCHAR;
|
|
||||||
v_year INT;
|
|
||||||
v_task_name TEXT;
|
|
||||||
v_phase_id TEXT;
|
|
||||||
v_utc_offset VARCHAR(3);
|
|
||||||
v_opens_at timestamptz;
|
|
||||||
v_closes_at timestamptz;
|
|
||||||
BEGIN
|
|
||||||
SELECT * INTO v_old
|
|
||||||
FROM v3.form_phase
|
|
||||||
WHERE budget_form_id = p_budget_form_id
|
|
||||||
AND sheet = p_sheet
|
|
||||||
AND phase_code = p_phase_code;
|
|
||||||
|
|
||||||
IF NOT FOUND THEN
|
|
||||||
RAISE EXCEPTION 'form_phase не найден (form_id=%, sheet=%, phase=%)',
|
|
||||||
p_budget_form_id, p_sheet, p_phase_code;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year, ou.utc_offset
|
|
||||||
INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year, v_utc_offset
|
|
||||||
FROM v3.budget_form bf
|
|
||||||
LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
|
|
||||||
WHERE bf.id = p_budget_form_id;
|
|
||||||
v_task_name := trim(concat(
|
|
||||||
COALESCE(v_form_type_code, ''),
|
|
||||||
CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
|
|
||||||
CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
|
|
||||||
));
|
|
||||||
|
|
||||||
IF p_opens_at is not null THEN
|
|
||||||
v_opens_at = (p_opens_at::text || v_utc_offset)::timestamptz;
|
|
||||||
END IF;
|
|
||||||
IF p_closes_at is not null THEN
|
|
||||||
v_closes_at = (p_closes_at::text || v_utc_offset)::timestamptz;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
UPDATE v3.form_phase
|
|
||||||
SET role = COALESCE(p_role, role),
|
|
||||||
column_keys = COALESCE(p_column_keys, column_keys),
|
|
||||||
opens_at = COALESCE(v_opens_at, opens_at),
|
|
||||||
closes_at = COALESCE(v_closes_at, closes_at)
|
|
||||||
WHERE budget_form_id = p_budget_form_id
|
|
||||||
AND sheet = p_sheet
|
|
||||||
AND phase_code = p_phase_code
|
|
||||||
RETURNING * INTO v_new;
|
|
||||||
|
|
||||||
|
|
||||||
v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code);
|
|
||||||
|
|
||||||
v_is_extend := v_new.closes_at > v_old.closes_at;
|
|
||||||
v_has_non_extend_change := v_old.role IS DISTINCT FROM v_new.role
|
|
||||||
OR v_old.column_keys IS DISTINCT FROM v_new.column_keys
|
|
||||||
OR v_old.opens_at IS DISTINCT FROM v_new.opens_at;
|
|
||||||
v_has_window_change := v_has_non_extend_change
|
|
||||||
OR (v_old.closes_at IS DISTINCT FROM v_new.closes_at AND NOT v_is_extend);
|
|
||||||
|
|
||||||
IF v_old.role IS DISTINCT FROM v_new.role THEN
|
|
||||||
v_changes := v_changes || jsonb_build_object(
|
|
||||||
'role', jsonb_build_object('before', v_old.role, 'after', v_new.role)
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
IF v_old.column_keys IS DISTINCT FROM v_new.column_keys THEN
|
|
||||||
v_changes := v_changes || jsonb_build_object(
|
|
||||||
'column_keys', jsonb_build_object('before', v_old.column_keys, 'after', v_new.column_keys)
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
IF v_old.opens_at IS DISTINCT FROM v_new.opens_at THEN
|
|
||||||
v_changes := v_changes || jsonb_build_object(
|
|
||||||
'opens_at', jsonb_build_object(
|
|
||||||
'before', v_old.opens_at,
|
|
||||||
'after', v_new.opens_at
|
|
||||||
)
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
IF v_old.closes_at IS DISTINCT FROM v_new.closes_at THEN
|
|
||||||
v_changes := v_changes || jsonb_build_object(
|
|
||||||
'closes_at', jsonb_build_object(
|
|
||||||
'before', v_old.closes_at,
|
|
||||||
'after', v_new.closes_at
|
|
||||||
)
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF v_is_extend THEN
|
|
||||||
PERFORM v3.log_event(
|
|
||||||
'ACCESS_EXTEND', 'ACCESS',
|
|
||||||
jsonb_build_object(
|
|
||||||
'entity_type', 'task',
|
|
||||||
'entity_id', p_budget_form_id,
|
|
||||||
'core_entity_type', 'form_phase',
|
|
||||||
'core_entity_id', v_phase_id,
|
|
||||||
|
|
||||||
'phase_id', v_phase_id,
|
|
||||||
'phase_name', p_phase_code,
|
|
||||||
'task_id', p_budget_form_id,
|
|
||||||
'task_name', NULLIF(v_task_name, ''),
|
|
||||||
'form_id', p_budget_form_id,
|
|
||||||
'budget_form_id', p_budget_form_id,
|
|
||||||
'org_unit_id', v_org_unit_id,
|
|
||||||
'org_unit_name', v_org_unit_title,
|
|
||||||
'sheet', p_sheet,
|
|
||||||
'phase_code', p_phase_code,
|
|
||||||
'closes_at_before', v_old.closes_at,
|
|
||||||
'closes_at_after', v_new.closes_at
|
|
||||||
),
|
|
||||||
p_budget_form_id
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF v_has_window_change THEN
|
|
||||||
PERFORM v3.log_event(
|
|
||||||
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
|
||||||
jsonb_build_object(
|
|
||||||
'entity_type', 'task',
|
|
||||||
'entity_id', p_budget_form_id,
|
|
||||||
'core_entity_type', 'form_phase',
|
|
||||||
'core_entity_id', v_phase_id,
|
|
||||||
'action', 'update',
|
|
||||||
'phase_id', v_phase_id,
|
|
||||||
'phase_name', p_phase_code,
|
|
||||||
'task_id', p_budget_form_id,
|
|
||||||
'task_name', NULLIF(v_task_name, ''),
|
|
||||||
'form_id', p_budget_form_id,
|
|
||||||
'budget_form_id', p_budget_form_id,
|
|
||||||
'org_unit_id', v_org_unit_id,
|
|
||||||
'org_unit_name', v_org_unit_title,
|
|
||||||
'sheet', p_sheet,
|
|
||||||
'phase_code', p_phase_code,
|
|
||||||
'changes', v_changes
|
|
||||||
),
|
|
||||||
p_budget_form_id, null, v_org_unit_id
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RETURN v_new;
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
pass
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1123,9 +1123,9 @@ END;
|
|||||||
$function$
|
$function$
|
||||||
;
|
;
|
||||||
|
|
||||||
-- DROP FUNCTION v3.add_vsp(int4, varchar, varchar, varchar, date, varchar, int4, numeric, date, bool, bool, varchar, int4, varchar, text, varchar, date);
|
-- DROP FUNCTION v3.add_vsp(int4, varchar, varchar, varchar, date, varchar, int4, numeric, date, bool, bool, varchar, int4, varchar, text, varchar, int4, varchar, date);
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION v3.add_vsp(p_branch_id integer, p_reg_number character varying DEFAULT NULL::character varying, p_address character varying DEFAULT NULL::character varying, p_format character varying DEFAULT NULL::character varying, p_opened_at date DEFAULT NULL::date, p_placement_type character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_closed_at date DEFAULT NULL::date, p_is_active boolean DEFAULT true, p_is_deleted boolean DEFAULT false, p_system_code character varying DEFAULT NULL::character varying, p_created_by integer DEFAULT NULL::integer, p_vsp_type character varying DEFAULT NULL::character varying, p_notes text DEFAULT NULL::text, p_rent_contract_num character varying DEFAULT NULL::character varying, p_rent_end_date date DEFAULT NULL::date)
|
CREATE OR REPLACE FUNCTION v3.add_vsp(p_branch_id integer, p_reg_number character varying DEFAULT NULL::character varying, p_address character varying DEFAULT NULL::character varying, p_format character varying DEFAULT NULL::character varying, p_opened_at date DEFAULT NULL::date, p_placement_type character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_closed_at date DEFAULT NULL::date, p_is_active boolean DEFAULT true, p_is_deleted boolean DEFAULT false, p_system_code character varying DEFAULT NULL::character varying, p_created_by integer DEFAULT NULL::integer, p_vsp_type character varying DEFAULT NULL::character varying, p_notes text DEFAULT NULL::text, p_location_form character varying DEFAULT NULL::character varying, p_numbers integer DEFAULT NULL::integer, p_rent_contract_num character varying DEFAULT NULL::character varying, p_rent_end_date date DEFAULT NULL::date)
|
||||||
RETURNS integer
|
RETURNS integer
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
AS $function$
|
AS $function$
|
||||||
@ -1153,12 +1153,14 @@ BEGIN
|
|||||||
(branch_id, reg_number, address, format, opened_at,
|
(branch_id, reg_number, address, format, opened_at,
|
||||||
placement_type, staff_count, total_area, closed_at,
|
placement_type, staff_count, total_area, closed_at,
|
||||||
is_active, is_deleted, system_code, created_by,
|
is_active, is_deleted, system_code, created_by,
|
||||||
vsp_type, notes, rent_contract_num, rent_end_date)
|
vsp_type, notes, location_form, numbers,
|
||||||
|
rent_contract_num, rent_end_date)
|
||||||
VALUES
|
VALUES
|
||||||
(p_branch_id, p_reg_number, p_address, p_format, p_opened_at,
|
(p_branch_id, p_reg_number, p_address, p_format, p_opened_at,
|
||||||
p_placement_type, p_staff_count, p_total_area, p_closed_at,
|
p_placement_type, p_staff_count, p_total_area, p_closed_at,
|
||||||
p_is_active, p_is_deleted, p_system_code, p_created_by,
|
p_is_active, p_is_deleted, p_system_code, p_created_by,
|
||||||
p_vsp_type, p_notes, p_rent_contract_num, p_rent_end_date)
|
p_vsp_type, p_notes, p_location_form, p_numbers,
|
||||||
|
p_rent_contract_num, p_rent_end_date)
|
||||||
RETURNING id INTO v_id;
|
RETURNING id INTO v_id;
|
||||||
|
|
||||||
PERFORM v3.log_event(
|
PERFORM v3.log_event(
|
||||||
@ -1182,6 +1184,8 @@ BEGIN
|
|||||||
'created_by', p_created_by,
|
'created_by', p_created_by,
|
||||||
'vsp_type', p_vsp_type,
|
'vsp_type', p_vsp_type,
|
||||||
'notes', p_notes,
|
'notes', p_notes,
|
||||||
|
'location_form', p_location_form,
|
||||||
|
'numbers', p_numbers,
|
||||||
'rent_contract_num', p_rent_contract_num,
|
'rent_contract_num', p_rent_contract_num,
|
||||||
'rent_end_date', v3._diff_val(p_rent_end_date),
|
'rent_end_date', v3._diff_val(p_rent_end_date),
|
||||||
'user_email', v_actor_email,
|
'user_email', v_actor_email,
|
||||||
@ -1570,7 +1574,7 @@ AS $function$
|
|||||||
DECLARE
|
DECLARE
|
||||||
v_report_id INT;
|
v_report_id INT;
|
||||||
v_eid INT;
|
v_eid INT;
|
||||||
v_form_id INT;
|
v_org_id INT;
|
||||||
v_ei_path INT[];
|
v_ei_path INT[];
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Извлекаем report_id + ei ДО DELETE (нужен для path)
|
-- Извлекаем report_id + ei ДО DELETE (нужен для path)
|
||||||
@ -1584,33 +1588,21 @@ BEGIN
|
|||||||
|
|
||||||
v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]);
|
v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]);
|
||||||
|
|
||||||
SELECT bf.id
|
|
||||||
INTO v_form_id
|
|
||||||
FROM v3.rf_project_report r
|
|
||||||
JOIN v3.project p
|
|
||||||
ON p.id = r.project_id
|
|
||||||
LEFT JOIN v3.budget_form bf
|
|
||||||
ON bf.form_type_code = 'FORM_3'
|
|
||||||
AND bf.year = r.year
|
|
||||||
AND bf.org_unit_id = p.org_unit_id
|
|
||||||
WHERE r.id = v_report_id
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
-- Каскад (FK без ON DELETE CASCADE)
|
-- Каскад (FK без ON DELETE CASCADE)
|
||||||
DELETE FROM v3.rf_project_report_quarter WHERE rf_project_report_line_id = p_line_id;
|
DELETE FROM v3.rf_project_report_quarter WHERE rf_project_report_line_id = p_line_id;
|
||||||
DELETE FROM v3.rf_project_report_line WHERE id = p_line_id;
|
DELETE FROM v3.rf_project_report_line WHERE id = p_line_id;
|
||||||
|
|
||||||
|
SELECT org_unit_id INTO v_org_id FROM v3.project WHERE id = v_report_id;
|
||||||
-- Аудит: ROW_DELETE
|
-- Аудит: ROW_DELETE
|
||||||
PERFORM v3.log_event(
|
PERFORM v3.log_event(
|
||||||
'ROW_DELETE', 'ROW',
|
'ROW_DELETE', 'ROW',
|
||||||
jsonb_build_object(
|
jsonb_build_object(
|
||||||
'sheet', 'FORM_3',
|
'sheet', 'FORM_3',
|
||||||
'line_id', p_line_id,
|
'line_id', p_line_id,
|
||||||
'form_id', v_form_id,
|
'entity_id', p_line_id,
|
||||||
'report_id', v_report_id,
|
'report_id', v_report_id,
|
||||||
'expense_item_id', v_eid
|
'expense_item_id', v_eid
|
||||||
),
|
), null, null, v_org_id
|
||||||
v_form_id
|
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Возврат: только иерархия по пути удалённой (INPUT уже нет)
|
-- Возврат: только иерархия по пути удалённой (INPUT уже нет)
|
||||||
@ -2746,9 +2738,9 @@ END;
|
|||||||
$function$
|
$function$
|
||||||
;
|
;
|
||||||
|
|
||||||
-- DROP FUNCTION v3.upd_vsp(int4, int4, varchar, varchar, varchar, date, varchar, int4, numeric, date, bool, bool, varchar, int4, varchar, text, varchar, date);
|
-- DROP FUNCTION v3.upd_vsp(int4, int4, varchar, varchar, varchar, date, varchar, int4, numeric, date, bool, bool, varchar, int4, varchar, text, varchar, int4, varchar, date);
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION v3.upd_vsp(p_vsp_id integer, p_branch_id integer DEFAULT NULL::integer, p_reg_number character varying DEFAULT NULL::character varying, p_address character varying DEFAULT NULL::character varying, p_format character varying DEFAULT NULL::character varying, p_opened_at date DEFAULT NULL::date, p_placement_type character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_closed_at date DEFAULT NULL::date, p_is_active boolean DEFAULT NULL::boolean, p_is_deleted boolean DEFAULT NULL::boolean, p_system_code character varying DEFAULT NULL::character varying, p_updated_by integer DEFAULT NULL::integer, p_vsp_type character varying DEFAULT NULL::character varying, p_notes text DEFAULT NULL::text, p_rent_contract_num character varying DEFAULT NULL::character varying, p_rent_end_date date DEFAULT NULL::date)
|
CREATE OR REPLACE FUNCTION v3.upd_vsp(p_vsp_id integer, p_branch_id integer DEFAULT NULL::integer, p_reg_number character varying DEFAULT NULL::character varying, p_address character varying DEFAULT NULL::character varying, p_format character varying DEFAULT NULL::character varying, p_opened_at date DEFAULT NULL::date, p_placement_type character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_closed_at date DEFAULT NULL::date, p_is_active boolean DEFAULT NULL::boolean, p_is_deleted boolean DEFAULT NULL::boolean, p_system_code character varying DEFAULT NULL::character varying, p_updated_by integer DEFAULT NULL::integer, p_vsp_type character varying DEFAULT NULL::character varying, p_notes text DEFAULT NULL::text, p_location_form character varying DEFAULT NULL::character varying, p_numbers integer DEFAULT NULL::integer, p_rent_contract_num character varying DEFAULT NULL::character varying, p_rent_end_date date DEFAULT NULL::date)
|
||||||
RETURNS void
|
RETURNS void
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
AS $function$
|
AS $function$
|
||||||
@ -2766,7 +2758,8 @@ BEGIN
|
|||||||
SELECT v.branch_id, v.reg_number, v.address, v.format, v.opened_at,
|
SELECT v.branch_id, v.reg_number, v.address, v.format, v.opened_at,
|
||||||
v.placement_type, v.staff_count, v.total_area, v.closed_at,
|
v.placement_type, v.staff_count, v.total_area, v.closed_at,
|
||||||
v.is_active, v.is_deleted, v.system_code,
|
v.is_active, v.is_deleted, v.system_code,
|
||||||
v.vsp_type, v.notes, v.rent_contract_num, v.rent_end_date
|
v.vsp_type, v.notes, v.location_form, v.numbers,
|
||||||
|
v.rent_contract_num, v.rent_end_date
|
||||||
INTO v_old
|
INTO v_old
|
||||||
FROM v3.vsp v
|
FROM v3.vsp v
|
||||||
WHERE v.id = p_vsp_id;
|
WHERE v.id = p_vsp_id;
|
||||||
@ -2899,6 +2892,20 @@ BEGIN
|
|||||||
'after', v3._diff_val(p_notes)));
|
'after', v3._diff_val(p_notes)));
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
|
IF p_location_form IS NOT NULL AND p_location_form IS DISTINCT FROM v_old.location_form THEN
|
||||||
|
UPDATE v3.vsp SET location_form = p_location_form WHERE id = p_vsp_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('location_form',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.location_form),
|
||||||
|
'after', v3._diff_val(p_location_form)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_numbers IS NOT NULL AND p_numbers IS DISTINCT FROM v_old.numbers THEN
|
||||||
|
UPDATE v3.vsp SET numbers = p_numbers WHERE id = p_vsp_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('numbers',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.numbers),
|
||||||
|
'after', v3._diff_val(p_numbers)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
IF p_rent_contract_num IS NOT NULL AND p_rent_contract_num IS DISTINCT FROM v_old.rent_contract_num THEN
|
IF p_rent_contract_num IS NOT NULL AND p_rent_contract_num IS DISTINCT FROM v_old.rent_contract_num THEN
|
||||||
UPDATE v3.vsp SET rent_contract_num = p_rent_contract_num WHERE id = p_vsp_id;
|
UPDATE v3.vsp SET rent_contract_num = p_rent_contract_num WHERE id = p_vsp_id;
|
||||||
v_changes := v_changes || jsonb_build_object('rent_contract_num',
|
v_changes := v_changes || jsonb_build_object('rent_contract_num',
|
||||||
@ -4086,13 +4093,15 @@ CREATE OR REPLACE FUNCTION v3.v_form1_smeta(p_year integer, p_org_unit_id intege
|
|||||||
AS $function$
|
AS $function$
|
||||||
#variable_conflict use_column
|
#variable_conflict use_column
|
||||||
DECLARE
|
DECLARE
|
||||||
v_ahr INT; v_cap INT; v_oper INT;
|
v_ahr_s INT; v_ahr_d INT;
|
||||||
|
v_cap_s INT; v_cap_d INT;
|
||||||
|
v_oper_s INT; v_oper_d INT;
|
||||||
v_sections TEXT[] := ARRAY['plan','approved','q1','q2','q3','q4'];
|
v_sections TEXT[] := ARRAY['plan','approved','q1','q2','q3','q4'];
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Одна budget_form на лист содержит ОБА направления (budget_line.direction).
|
-- Resolve form_ids: within each (year, ssp, sheet), lower bf.id = Support, higher = Development
|
||||||
-- Support/Development различаются параметром p_direction, а не отдельной формой.
|
|
||||||
WITH ranked AS (
|
WITH ranked AS (
|
||||||
SELECT bf.id AS fid, ei.sheet AS sh
|
SELECT bf.id AS fid, ei.sheet AS sh,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY ei.sheet ORDER BY bf.id) AS rn
|
||||||
FROM budget_form bf
|
FROM budget_form bf
|
||||||
JOIN budget_line bl ON bl.budget_form_id = bf.id
|
JOIN budget_line bl ON bl.budget_form_id = bf.id
|
||||||
JOIN expense_item ei ON ei.id = bl.expense_item_id
|
JOIN expense_item ei ON ei.id = bl.expense_item_id
|
||||||
@ -4100,14 +4109,18 @@ BEGIN
|
|||||||
GROUP BY bf.id, ei.sheet
|
GROUP BY bf.id, ei.sheet
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
MAX(CASE WHEN sh='AHR' THEN fid END),
|
MAX(CASE WHEN sh='AHR' AND rn=1 THEN fid END),
|
||||||
MAX(CASE WHEN sh='CAP' THEN fid END),
|
MAX(CASE WHEN sh='AHR' AND rn=2 THEN fid END),
|
||||||
MAX(CASE WHEN sh='OPER' THEN fid END)
|
MAX(CASE WHEN sh='CAP' AND rn=1 THEN fid END),
|
||||||
INTO v_ahr, v_cap, v_oper
|
MAX(CASE WHEN sh='CAP' AND rn=2 THEN fid END),
|
||||||
|
MAX(CASE WHEN sh='OPER' AND rn=1 THEN fid END),
|
||||||
|
MAX(CASE WHEN sh='OPER' AND rn=2 THEN fid END)
|
||||||
|
INTO v_ahr_s, v_ahr_d, v_cap_s, v_cap_d, v_oper_s, v_oper_d
|
||||||
FROM ranked;
|
FROM ranked;
|
||||||
|
|
||||||
RETURN QUERY
|
RETURN QUERY
|
||||||
WITH
|
WITH
|
||||||
|
-- 5 источников: row_type, depth, section_code, name + 22 метрик из sections
|
||||||
ahr_s AS (
|
ahr_s AS (
|
||||||
SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type,
|
SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type,
|
||||||
col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr,
|
col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr,
|
||||||
@ -4115,7 +4128,7 @@ BEGIN
|
|||||||
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
||||||
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
||||||
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
||||||
FROM v_form1_sheet_sections(v_ahr, 'AHR', 'Support', v_sections)
|
FROM v_form1_sheet_sections(v_ahr_s, 'AHR', 'Support', v_sections)
|
||||||
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
||||||
),
|
),
|
||||||
ahr_d AS (
|
ahr_d AS (
|
||||||
@ -4125,7 +4138,7 @@ BEGIN
|
|||||||
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
||||||
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
||||||
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
||||||
FROM v_form1_sheet_sections(v_ahr, 'AHR', 'Development', v_sections)
|
FROM v_form1_sheet_sections(v_ahr_d, 'AHR', 'Development', v_sections)
|
||||||
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
||||||
),
|
),
|
||||||
cap_s AS (
|
cap_s AS (
|
||||||
@ -4135,7 +4148,7 @@ BEGIN
|
|||||||
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
||||||
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
||||||
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
||||||
FROM v_form1_sheet_sections(v_cap, 'CAP', 'Support', v_sections)
|
FROM v_form1_sheet_sections(v_cap_s, 'CAP', 'Support', v_sections)
|
||||||
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
||||||
),
|
),
|
||||||
cap_d AS (
|
cap_d AS (
|
||||||
@ -4145,7 +4158,7 @@ BEGIN
|
|||||||
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
||||||
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
||||||
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
||||||
FROM v_form1_sheet_sections(v_cap, 'CAP', 'Development', v_sections)
|
FROM v_form1_sheet_sections(v_cap_d, 'CAP', 'Development', v_sections)
|
||||||
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
||||||
),
|
),
|
||||||
oper_s AS (
|
oper_s AS (
|
||||||
@ -4155,10 +4168,12 @@ BEGIN
|
|||||||
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
||||||
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
||||||
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
||||||
FROM v_form1_sheet_sections(v_oper, 'OPER', 'Support', v_sections)
|
FROM v_form1_sheet_sections(v_oper_s, 'OPER', 'Support', v_sections)
|
||||||
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
||||||
),
|
),
|
||||||
|
-- Пивот по section_code
|
||||||
merged AS (
|
merged AS (
|
||||||
|
-- AHR блок
|
||||||
SELECT COALESCE(s.row_type, d.row_type) AS row_type,
|
SELECT COALESCE(s.row_type, d.row_type) AS row_type,
|
||||||
COALESCE(s.depth, d.depth) AS depth,
|
COALESCE(s.depth, d.depth) AS depth,
|
||||||
COALESCE(s.sc, d.sc) AS sc,
|
COALESCE(s.sc, d.sc) AS sc,
|
||||||
@ -4173,6 +4188,7 @@ BEGIN
|
|||||||
d.np2 AS d_np2, d.np3 AS d_np3, d.np4 AS d_np4
|
d.np2 AS d_np2, d.np3 AS d_np3, d.np4 AS d_np4
|
||||||
FROM ahr_s s FULL OUTER JOIN ahr_d d ON s.sc = d.sc
|
FROM ahr_s s FULL OUTER JOIN ahr_d d ON s.sc = d.sc
|
||||||
UNION ALL
|
UNION ALL
|
||||||
|
-- CAP блок
|
||||||
SELECT COALESCE(s.row_type, d.row_type), COALESCE(s.depth, d.depth),
|
SELECT COALESCE(s.row_type, d.row_type), COALESCE(s.depth, d.depth),
|
||||||
COALESCE(s.sc, d.sc), COALESCE(s.nm, d.nm),
|
COALESCE(s.sc, d.sc), COALESCE(s.nm, d.nm),
|
||||||
s.pq1, s.pq2, s.pq3, s.pq4, s.pyr,
|
s.pq1, s.pq2, s.pq3, s.pq4, s.pyr,
|
||||||
@ -4185,6 +4201,7 @@ BEGIN
|
|||||||
d.np2, d.np3, d.np4
|
d.np2, d.np3, d.np4
|
||||||
FROM cap_s s FULL OUTER JOIN cap_d d ON s.sc = d.sc
|
FROM cap_s s FULL OUTER JOIN cap_d d ON s.sc = d.sc
|
||||||
UNION ALL
|
UNION ALL
|
||||||
|
-- OPER блок (только Support)
|
||||||
SELECT s.row_type, s.depth, s.sc, s.nm,
|
SELECT s.row_type, s.depth, s.sc, s.nm,
|
||||||
s.pq1, s.pq2, s.pq3, s.pq4, s.pyr,
|
s.pq1, s.pq2, s.pq3, s.pq4, s.pyr,
|
||||||
NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC,
|
NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC,
|
||||||
@ -4215,6 +4232,7 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$function$
|
$function$
|
||||||
;
|
;
|
||||||
|
|
||||||
-- DROP FUNCTION v3.v_form2_sheet_sections(int4, varchar, _text);
|
-- DROP FUNCTION v3.v_form2_sheet_sections(int4, varchar, _text);
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION v3.v_form2_sheet_sections(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[])
|
CREATE OR REPLACE FUNCTION v3.v_form2_sheet_sections(p_form_id integer, p_sheet character varying, p_sections text[] DEFAULT NULL::text[])
|
||||||
@ -4409,7 +4427,7 @@ BEGIN
|
|||||||
LEFT JOIN v3.sequestration sg ON sg.line_id = bl.id AND sg.actor='SSP_GO' AND s_need_ap
|
LEFT JOIN v3.sequestration sg ON sg.line_id = bl.id AND sg.actor='SSP_GO' AND s_need_ap
|
||||||
LEFT JOIN v3.reserve r ON r.line_id = bl.id AND s_need_ap
|
LEFT JOIN v3.reserve r ON r.line_id = bl.id AND s_need_ap
|
||||||
LEFT JOIN v3.ckk ck ON ck.line_id = bl.id AND s_book
|
LEFT JOIN v3.ckk ck ON ck.line_id = bl.id AND s_book
|
||||||
WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet
|
WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet and (p_direction is null OR bl.direction is null or bl.direction = p_direction)
|
||||||
AND (s_need_ap OR s_book)
|
AND (s_need_ap OR s_book)
|
||||||
GROUP BY bl.expense_item_id
|
GROUP BY bl.expense_item_id
|
||||||
),
|
),
|
||||||
@ -4435,7 +4453,7 @@ BEGIN
|
|||||||
FROM v3.budget_line bl
|
FROM v3.budget_line bl
|
||||||
JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
|
JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
|
||||||
JOIN v3.budget_line_quarter q ON q.line_id = bl.id
|
JOIN v3.budget_line_quarter q ON q.line_id = bl.id
|
||||||
WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet
|
WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet and (p_direction is null OR bl.direction is null or bl.direction = p_direction)
|
||||||
GROUP BY bl.expense_item_id, q.quarter
|
GROUP BY bl.expense_item_id, q.quarter
|
||||||
),
|
),
|
||||||
-- Tree-rollup
|
-- Tree-rollup
|
||||||
@ -7773,406 +7791,3 @@ 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;
|
|
||||||
|
|
||||||
|
|||||||
@ -4251,28 +4251,3 @@ 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;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,852 +0,0 @@
|
|||||||
-- ════════════════════════════════════════════════════════════════════════════
|
|
||||||
-- 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,4 +14,3 @@ aiosqlite==0.20.0
|
|||||||
asyncpg==0.30.0
|
asyncpg==0.30.0
|
||||||
pytest==8.3.2
|
pytest==8.3.2
|
||||||
pytest-asyncio==0.24.0
|
pytest-asyncio==0.24.0
|
||||||
openpyxl==3.1.5
|
|
||||||
|
|||||||
@ -36,9 +36,9 @@ async def get_user_by_token(
|
|||||||
|
|
||||||
user_repo = UserRepository(db)
|
user_repo = UserRepository(db)
|
||||||
user = await user_repo.get_by_username(username)
|
user = await user_repo.get_by_username(username)
|
||||||
if user is None or not user.is_active:
|
if user is None:
|
||||||
user = await user_repo.get_by_email(username)
|
user = await user_repo.get_by_email(username)
|
||||||
if user is None or not user.is_active:
|
if user is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Пользователь не найден",
|
detail="Пользователь не найден",
|
||||||
|
|||||||
@ -2,13 +2,11 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.db.models.form_type import FormTypeEnum
|
|
||||||
from src.services.expense_item_service import ExpenseItemService
|
from src.services.expense_item_service import ExpenseItemService
|
||||||
from src.db.models.app_user import AppUser
|
from src.db.models.app_user import AppUser
|
||||||
from src.api.v1.deps import require_admin, require_executor
|
from src.api.v1.deps import require_admin
|
||||||
from src.db.session import get_db
|
from src.db.session import get_db
|
||||||
from src.domain.schemas import (
|
from src.domain.schemas import (
|
||||||
BaseListResponse,
|
|
||||||
BaseSingleResponse,
|
BaseSingleResponse,
|
||||||
ExpenseItemResponseSchema,
|
ExpenseItemResponseSchema,
|
||||||
)
|
)
|
||||||
@ -35,28 +33,3 @@ async def get_expense_item_by_id(
|
|||||||
message="Запись Expense Item",
|
message="Запись Expense Item",
|
||||||
result=ExpenseItemResponseSchema.model_validate(result),
|
result=ExpenseItemResponseSchema.model_validate(result),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=BaseListResponse[ExpenseItemResponseSchema])
|
|
||||||
async def get_expense_item_by_id(
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user: AppUser = Depends(require_executor),
|
|
||||||
r_start: bool | None = None,
|
|
||||||
sheet: str | None = None,
|
|
||||||
direction: str | None = None,
|
|
||||||
form_type: FormTypeEnum | str | None = None
|
|
||||||
):
|
|
||||||
"""Получение записей expense item"""
|
|
||||||
service = ExpenseItemService(db)
|
|
||||||
result = await service.get_list(
|
|
||||||
r_start=r_start,
|
|
||||||
sheet=sheet,
|
|
||||||
direction=direction,
|
|
||||||
form_type=form_type,
|
|
||||||
)
|
|
||||||
|
|
||||||
return BaseListResponse(
|
|
||||||
success=True,
|
|
||||||
message="Запись Expense Item",
|
|
||||||
result=[ExpenseItemResponseSchema.model_validate(el) for el in result],
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,19 +1,10 @@
|
|||||||
import datetime
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.services.project_service import ProjectService
|
|
||||||
from src.services.form3_phase_service import Form3PhaseService
|
|
||||||
from src.services.rf_project_report_service import RfProjectReportService
|
|
||||||
from src.services.org_unit_service import OrgUnitService
|
|
||||||
from src.db.models.app_user import AppUser
|
from src.db.models.app_user import AppUser
|
||||||
from src.domain.schemas import (
|
from src.domain.schemas import (
|
||||||
BaseListResponse,
|
BaseListResponse,
|
||||||
BaseSingleResponse,
|
BaseSingleResponse,
|
||||||
Form3PhaseCreate,
|
|
||||||
Form3PhaseResponse,
|
|
||||||
Form3PhaseUpdate,
|
|
||||||
FormPhaseCreate,
|
FormPhaseCreate,
|
||||||
FormPhaseResponse,
|
FormPhaseResponse,
|
||||||
FormPhaseUpdate,
|
FormPhaseUpdate,
|
||||||
@ -29,18 +20,6 @@ from src.db.session import get_db
|
|||||||
router = APIRouter(prefix="/stages", tags=["stages"])
|
router = APIRouter(prefix="/stages", tags=["stages"])
|
||||||
|
|
||||||
|
|
||||||
def timezone_from_offset(offset: str) -> datetime.timezone | None:
|
|
||||||
if not offset:
|
|
||||||
return None
|
|
||||||
|
|
||||||
hours = int(offset[1:])
|
|
||||||
return datetime.timezone(
|
|
||||||
datetime.timedelta(
|
|
||||||
hours=hours if offset[0] == '+' else -hours
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/form/{form_id}")
|
@router.get("/form/{form_id}")
|
||||||
async def get_form_phases(
|
async def get_form_phases(
|
||||||
form_id: int,
|
form_id: int,
|
||||||
@ -50,30 +29,14 @@ async def get_form_phases(
|
|||||||
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||||
) -> BaseListResponse[FormPhaseResponse]:
|
) -> BaseListResponse[FormPhaseResponse]:
|
||||||
fp_service = FormPhaseService(db)
|
fp_service = FormPhaseService(db)
|
||||||
org_unit_service = OrgUnitService(db)
|
|
||||||
bf_service = BudgetFormService(db)
|
|
||||||
|
|
||||||
phases = await fp_service.get_list(
|
phases = await fp_service.get_list(
|
||||||
budget_form_id=form_id,
|
budget_form_id=form_id,
|
||||||
user=current_user,
|
user=current_user,
|
||||||
sheet=sheet,
|
sheet=sheet,
|
||||||
phase_code=phase_code,
|
phase_code=phase_code,
|
||||||
)
|
)
|
||||||
if phases:
|
|
||||||
form = await bf_service.get(user=current_user, budget_form_id=form_id)
|
|
||||||
org_unit = await org_unit_service.get(user=current_user, org_unit_id=form.org_unit_id)
|
|
||||||
result = [FormPhaseResponse.model_validate(p) for p in phases]
|
|
||||||
if org_unit.utc_offset:
|
|
||||||
tz = timezone_from_offset(org_unit.utc_offset)
|
|
||||||
for element in result:
|
|
||||||
if element.opens_at:
|
|
||||||
element.opens_at = element.opens_at.astimezone(tz)
|
|
||||||
if element.closes_at:
|
|
||||||
element.closes_at = element.closes_at.astimezone(tz)
|
|
||||||
else:
|
|
||||||
result = []
|
|
||||||
return BaseListResponse(
|
return BaseListResponse(
|
||||||
result=result,
|
result=[FormPhaseResponse.model_validate(p) for p in phases],
|
||||||
count=len(phases),
|
count=len(phases),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -90,25 +53,13 @@ async def create_form_phase(
|
|||||||
if not form:
|
if not form:
|
||||||
raise HTTPException(404, "Форма не найдена")
|
raise HTTPException(404, "Форма не найдена")
|
||||||
|
|
||||||
org_unit_service = OrgUnitService(db)
|
|
||||||
org_unit = await org_unit_service.get(user=current_user, org_unit_id=form.org_unit_id)
|
|
||||||
|
|
||||||
fp_service = FormPhaseService(db)
|
fp_service = FormPhaseService(db)
|
||||||
phase = await fp_service.create(
|
phase = await fp_service.create(
|
||||||
budget_form=form,
|
budget_form=form,
|
||||||
body=body,
|
body=body,
|
||||||
user=current_user,
|
user=current_user,
|
||||||
)
|
)
|
||||||
|
return BaseSingleResponse(result=FormPhaseResponse.model_validate(phase))
|
||||||
result = FormPhaseResponse.model_validate(phase)
|
|
||||||
|
|
||||||
if org_unit.utc_offset:
|
|
||||||
tz = timezone_from_offset(org_unit.utc_offset)
|
|
||||||
if result.opens_at:
|
|
||||||
result.opens_at = result.opens_at.astimezone(tz)
|
|
||||||
if result.closes_at:
|
|
||||||
result.closes_at = result.closes_at.astimezone(tz)
|
|
||||||
return BaseSingleResponse(result=result)
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/form/{form_id}/{sheet}/{phase_code}")
|
@router.patch("/form/{form_id}/{sheet}/{phase_code}")
|
||||||
@ -133,21 +84,9 @@ async def update_form_phase(
|
|||||||
body=body,
|
body=body,
|
||||||
user=current_user,
|
user=current_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not phase:
|
if not phase:
|
||||||
raise HTTPException(404, "Этап не найден")
|
raise HTTPException(404, "Этап не найден")
|
||||||
|
return BaseSingleResponse(result=FormPhaseResponse.model_validate(phase))
|
||||||
org_unit_service = OrgUnitService(db)
|
|
||||||
org_unit = await org_unit_service.get(user=current_user, org_unit_id=form.org_unit_id)
|
|
||||||
result = FormPhaseResponse.model_validate(phase)
|
|
||||||
|
|
||||||
if org_unit.utc_offset:
|
|
||||||
tz = timezone_from_offset(org_unit.utc_offset)
|
|
||||||
if result.opens_at:
|
|
||||||
result.opens_at = result.opens_at.astimezone(tz)
|
|
||||||
if result.closes_at:
|
|
||||||
result.closes_at = result.closes_at.astimezone(tz)
|
|
||||||
return BaseSingleResponse(result=result)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/form/{form_id}/{sheet}/{phase_code}")
|
@router.delete("/form/{form_id}/{sheet}/{phase_code}")
|
||||||
@ -172,173 +111,3 @@ async def delete_form_phase(
|
|||||||
)
|
)
|
||||||
if not deleted:
|
if not deleted:
|
||||||
raise HTTPException(404, "Этап не найден")
|
raise HTTPException(404, "Этап не найден")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/project/{project_id}")
|
|
||||||
async def get_form3_phases(
|
|
||||||
project_id: int,
|
|
||||||
year: int | None = None,
|
|
||||||
report_type: str | None = None,
|
|
||||||
phase_code: str | None = None,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
|
||||||
) -> BaseListResponse[Form3PhaseResponse]:
|
|
||||||
f3p_service = Form3PhaseService(db)
|
|
||||||
report_service = RfProjectReportService(db)
|
|
||||||
project_service = ProjectService(db)
|
|
||||||
org_unit_service = OrgUnitService(db)
|
|
||||||
|
|
||||||
reports = await report_service.get_list(
|
|
||||||
project_id=project_id,
|
|
||||||
year=year,
|
|
||||||
report_type=report_type,
|
|
||||||
)
|
|
||||||
|
|
||||||
phases = await f3p_service.get_list(
|
|
||||||
report_id=[el.id for el in reports],
|
|
||||||
phase_code=phase_code,
|
|
||||||
)
|
|
||||||
if phases:
|
|
||||||
project = await project_service.get_instance(user=current_user, project_id=project_id)
|
|
||||||
org_unit = await org_unit_service.get(user=current_user, org_unit_id=project.org_unit_id)
|
|
||||||
result = [Form3PhaseResponse.model_validate(p) for p in phases]
|
|
||||||
if org_unit.utc_offset:
|
|
||||||
tz = timezone_from_offset(org_unit.utc_offset)
|
|
||||||
for element in result:
|
|
||||||
if element.opens_at:
|
|
||||||
element.opens_at = element.opens_at.astimezone(tz)
|
|
||||||
if element.closes_at:
|
|
||||||
element.closes_at = element.closes_at.astimezone(tz)
|
|
||||||
else:
|
|
||||||
result = []
|
|
||||||
if result:
|
|
||||||
reports = {report.id: report for report in reports}
|
|
||||||
for i, element in enumerate(result):
|
|
||||||
element.year = reports[phases[i].rf_project_report_id].year
|
|
||||||
element.report_type = reports[phases[i].rf_project_report_id].report_type
|
|
||||||
return BaseListResponse(
|
|
||||||
result=result,
|
|
||||||
count=len(phases),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/project/{project_id}")
|
|
||||||
async def create_form3_phase(
|
|
||||||
project_id: int,
|
|
||||||
body: Form3PhaseCreate,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user: AppUser = Depends(require_admin),
|
|
||||||
) -> BaseSingleResponse[Form3PhaseResponse]:
|
|
||||||
report_service = RfProjectReportService(db)
|
|
||||||
report = await report_service.get(
|
|
||||||
project_id=project_id,
|
|
||||||
report_type=body.report_type,
|
|
||||||
year=body.year,
|
|
||||||
user=current_user,
|
|
||||||
load_project=True,
|
|
||||||
)
|
|
||||||
if not report:
|
|
||||||
raise HTTPException(404, "Форма не найдена")
|
|
||||||
|
|
||||||
org_unit_service = OrgUnitService(db)
|
|
||||||
org_unit = await org_unit_service.get(user=current_user, org_unit_id=report.project.org_unit_id)
|
|
||||||
|
|
||||||
f3p_service = Form3PhaseService(db)
|
|
||||||
phase = await f3p_service.create(
|
|
||||||
report_id=report.id,
|
|
||||||
body=body,
|
|
||||||
user=current_user,
|
|
||||||
)
|
|
||||||
|
|
||||||
result = Form3PhaseResponse.model_validate(phase)
|
|
||||||
|
|
||||||
if org_unit.utc_offset:
|
|
||||||
tz = timezone_from_offset(org_unit.utc_offset)
|
|
||||||
if result.opens_at:
|
|
||||||
result.opens_at = result.opens_at.astimezone(tz)
|
|
||||||
if result.closes_at:
|
|
||||||
result.closes_at = result.closes_at.astimezone(tz)
|
|
||||||
|
|
||||||
result.year = body.year
|
|
||||||
result.report_type = body.report_type
|
|
||||||
|
|
||||||
return BaseSingleResponse(result=result)
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/project/{project_id}/{year}/{report_type}/{phase_code}")
|
|
||||||
async def update_form3_phase(
|
|
||||||
project_id: int,
|
|
||||||
year: int,
|
|
||||||
report_type: str,
|
|
||||||
phase_code: str,
|
|
||||||
body: Form3PhaseUpdate,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user: AppUser = Depends(require_admin),
|
|
||||||
) -> BaseSingleResponse[Form3PhaseResponse]:
|
|
||||||
report_service = RfProjectReportService(db)
|
|
||||||
report = await report_service.get(
|
|
||||||
project_id=project_id,
|
|
||||||
report_type=report_type,
|
|
||||||
year=year,
|
|
||||||
user=current_user,
|
|
||||||
load_project=True,
|
|
||||||
)
|
|
||||||
if not report:
|
|
||||||
raise HTTPException(404, "Форма не найдена")
|
|
||||||
|
|
||||||
f3p_service = Form3PhaseService(db)
|
|
||||||
phase = await f3p_service.update(
|
|
||||||
report_id=report.id,
|
|
||||||
phase_code=phase_code,
|
|
||||||
body=body,
|
|
||||||
user=current_user,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not phase:
|
|
||||||
raise HTTPException(404, "Этап не найден")
|
|
||||||
|
|
||||||
org_unit_service = OrgUnitService(db)
|
|
||||||
org_unit = await org_unit_service.get(user=current_user, org_unit_id=report.project.org_unit_id)
|
|
||||||
result = Form3PhaseResponse.model_validate(phase)
|
|
||||||
|
|
||||||
if org_unit.utc_offset:
|
|
||||||
tz = timezone_from_offset(org_unit.utc_offset)
|
|
||||||
if result.opens_at:
|
|
||||||
result.opens_at = result.opens_at.astimezone(tz)
|
|
||||||
if result.closes_at:
|
|
||||||
result.closes_at = result.closes_at.astimezone(tz)
|
|
||||||
|
|
||||||
result.year = year
|
|
||||||
result.report_type = report_type
|
|
||||||
|
|
||||||
return BaseSingleResponse(result=result)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/project/{project_id}/{year}/{report_type}/{phase_code}")
|
|
||||||
async def delete_form3_phase(
|
|
||||||
project_id: int,
|
|
||||||
year: int,
|
|
||||||
report_type: str,
|
|
||||||
phase_code: str,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
current_user: AppUser = Depends(require_admin),
|
|
||||||
):
|
|
||||||
report_service = RfProjectReportService(db)
|
|
||||||
report = await report_service.get(
|
|
||||||
project_id=project_id,
|
|
||||||
report_type=report_type,
|
|
||||||
year=year,
|
|
||||||
user=current_user,
|
|
||||||
load_project=True,
|
|
||||||
)
|
|
||||||
if not report:
|
|
||||||
raise HTTPException(404, "Форма не найдена")
|
|
||||||
|
|
||||||
f3p_service = Form3PhaseService(db)
|
|
||||||
deleted = await f3p_service.delete(
|
|
||||||
report_id=report.id,
|
|
||||||
phase_code=phase_code,
|
|
||||||
user=current_user,
|
|
||||||
)
|
|
||||||
if not deleted:
|
|
||||||
raise HTTPException(404, "Этап не найден")
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import logging
|
|
||||||
import time
|
import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@ -21,9 +20,6 @@ SHEETS_WITH_SECTIONS = {"AHR", "CAP", "OPER"}
|
|||||||
FORM1_DIRECTION_REQUIRED_SHEETS = {"AHR", "CAP"}
|
FORM1_DIRECTION_REQUIRED_SHEETS = {"AHR", "CAP"}
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
|
||||||
|
|
||||||
|
|
||||||
def _rows_to_sheet_response(result: list[tuple]) -> list[SheetResponse]:
|
def _rows_to_sheet_response(result: list[tuple]) -> list[SheetResponse]:
|
||||||
return [
|
return [
|
||||||
SheetResponse(
|
SheetResponse(
|
||||||
@ -157,20 +153,6 @@ async def get_sheet(
|
|||||||
)
|
)
|
||||||
db_ms = (time.perf_counter() - t0) * 1000
|
db_ms = (time.perf_counter() - t0) * 1000
|
||||||
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
||||||
extra={
|
|
||||||
"type": "db_time",
|
|
||||||
"time_ms": f"{db_ms:.2f}",
|
|
||||||
"handler": "get_sheet",
|
|
||||||
"form_id": form_id,
|
|
||||||
"sheet": sheet,
|
|
||||||
"direction": direction,
|
|
||||||
"sections": sections,
|
|
||||||
"user": current_user.id,
|
|
||||||
}
|
|
||||||
logger.info(
|
|
||||||
f"db_time: {extra}",
|
|
||||||
extra=extra,
|
|
||||||
)
|
|
||||||
return BaseListResponse(
|
return BaseListResponse(
|
||||||
count=len(result),
|
count=len(result),
|
||||||
result=_rows_to_sheet_response(result),
|
result=_rows_to_sheet_response(result),
|
||||||
@ -206,6 +188,12 @@ async def update_cell(
|
|||||||
if sections and (rem_sects := set(sections) - set(form.form_type.section_list)):
|
if sections and (rem_sects := set(sections) - set(form.form_type.section_list)):
|
||||||
raise HTTPException(400, f"Недопустимые sections: {', '.join(sorted(rem_sects))}")
|
raise HTTPException(400, f"Недопустимые sections: {', '.join(sorted(rem_sects))}")
|
||||||
|
|
||||||
|
budget_line_service = BudgetLineService(db)
|
||||||
|
budget_line = await budget_line_service.get(budget_line_id=cell_body.line_id, user=current_user)
|
||||||
|
if not budget_line or budget_line.budget_form_id != form_id:
|
||||||
|
raise HTTPException(404, f"Строка формы не найдена")
|
||||||
|
|
||||||
|
|
||||||
result = await sheet_service.update_cell(
|
result = await sheet_service.update_cell(
|
||||||
form_id=form_id,
|
form_id=form_id,
|
||||||
sheet=sheet,
|
sheet=sheet,
|
||||||
@ -218,22 +206,6 @@ async def update_cell(
|
|||||||
)
|
)
|
||||||
db_ms = (time.perf_counter() - t0) * 1000
|
db_ms = (time.perf_counter() - t0) * 1000
|
||||||
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
||||||
extra={
|
|
||||||
"type": "db_time",
|
|
||||||
"time_ms": f"{db_ms:.2f}",
|
|
||||||
"handler": "update_form_cell",
|
|
||||||
"form_id": form_id,
|
|
||||||
"sheet": sheet,
|
|
||||||
"direction": direction,
|
|
||||||
"sections": sections,
|
|
||||||
"user": current_user.id,
|
|
||||||
"line_id": cell_body.line_id,
|
|
||||||
"column": cell_body.column,
|
|
||||||
}
|
|
||||||
logger.info(
|
|
||||||
f"db_time: {extra}",
|
|
||||||
extra=extra,
|
|
||||||
)
|
|
||||||
return BaseListResponse(
|
return BaseListResponse(
|
||||||
count=len(result),
|
count=len(result),
|
||||||
result=_rows_to_sheet_response(result),
|
result=_rows_to_sheet_response(result),
|
||||||
@ -271,6 +243,14 @@ async def update_cells(
|
|||||||
if sections and (rem_sects := set(sections) - set(form.form_type.section_list)):
|
if sections and (rem_sects := set(sections) - set(form.form_type.section_list)):
|
||||||
raise HTTPException(400, f"Недопустимые sections: {', '.join(sorted(rem_sects))}")
|
raise HTTPException(400, f"Недопустимые sections: {', '.join(sorted(rem_sects))}")
|
||||||
|
|
||||||
|
budget_line_service = BudgetLineService(db)
|
||||||
|
budget_lines = await budget_line_service.get_list(
|
||||||
|
budget_line_ids=[cb.line_id for cb in cells_body.changes], user=current_user)
|
||||||
|
for budget_line in budget_lines:
|
||||||
|
if budget_line.budget_form_id != form_id:
|
||||||
|
raise HTTPException(404, f"Строка формы не найдена")
|
||||||
|
|
||||||
|
|
||||||
result = await sheet_service.update_cells(
|
result = await sheet_service.update_cells(
|
||||||
form_id=form_id,
|
form_id=form_id,
|
||||||
sheet=sheet,
|
sheet=sheet,
|
||||||
@ -281,21 +261,6 @@ async def update_cells(
|
|||||||
)
|
)
|
||||||
db_ms = (time.perf_counter() - t0) * 1000
|
db_ms = (time.perf_counter() - t0) * 1000
|
||||||
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
||||||
extra={
|
|
||||||
"type": "db_time",
|
|
||||||
"time_ms": f"{db_ms:.2f}",
|
|
||||||
"handler": "update_form_cells",
|
|
||||||
"form_id": form_id,
|
|
||||||
"sheet": sheet,
|
|
||||||
"direction": direction,
|
|
||||||
"sections": sections,
|
|
||||||
"user": current_user.id,
|
|
||||||
"count": len(cells_body.changes)
|
|
||||||
}
|
|
||||||
logger.info(
|
|
||||||
f"db_time: {extra}",
|
|
||||||
extra=extra,
|
|
||||||
)
|
|
||||||
return BaseListResponse(
|
return BaseListResponse(
|
||||||
count=len(result),
|
count=len(result),
|
||||||
result=_rows_to_sheet_response(result),
|
result=_rows_to_sheet_response(result),
|
||||||
@ -345,21 +310,6 @@ async def add_line(
|
|||||||
)
|
)
|
||||||
db_ms = (time.perf_counter() - t0) * 1000
|
db_ms = (time.perf_counter() - t0) * 1000
|
||||||
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
||||||
|
|
||||||
extra={
|
|
||||||
"type": "db_time",
|
|
||||||
"time_ms": f"{db_ms:.2f}",
|
|
||||||
"handler": "add_form_line",
|
|
||||||
"form_id": form_id,
|
|
||||||
"sheet": sheet,
|
|
||||||
"direction": body.direction,
|
|
||||||
"user": current_user.id,
|
|
||||||
"expense_item_id": body.expense_item_id
|
|
||||||
}
|
|
||||||
logger.info(
|
|
||||||
f"db_time: {extra}",
|
|
||||||
extra=extra,
|
|
||||||
)
|
|
||||||
return BaseListResponse(
|
return BaseListResponse(
|
||||||
count=len(result),
|
count=len(result),
|
||||||
result=_rows_to_sheet_response(result),
|
result=_rows_to_sheet_response(result),
|
||||||
@ -401,21 +351,6 @@ async def delete_line(
|
|||||||
)
|
)
|
||||||
db_ms = (time.perf_counter() - t0) * 1000
|
db_ms = (time.perf_counter() - t0) * 1000
|
||||||
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
||||||
extra={
|
|
||||||
"type": "db_time",
|
|
||||||
"time_ms": f"{db_ms:.2f}",
|
|
||||||
"handler": "del_form_line",
|
|
||||||
"form_id": form_id,
|
|
||||||
"sheet": sheet,
|
|
||||||
"direction": direction,
|
|
||||||
"user": current_user.id,
|
|
||||||
"line_id": row_id,
|
|
||||||
}
|
|
||||||
logger.info(
|
|
||||||
f"db_time: {extra}",
|
|
||||||
extra=extra,
|
|
||||||
)
|
|
||||||
|
|
||||||
return BaseListResponse(
|
return BaseListResponse(
|
||||||
count=len(result),
|
count=len(result),
|
||||||
result=_rows_to_sheet_response(result),
|
result=_rows_to_sheet_response(result),
|
||||||
@ -441,7 +376,7 @@ async def add_form(
|
|||||||
)
|
)
|
||||||
raise HTTPException(404, f"ССП {not_found_ids} не найдены")
|
raise HTTPException(404, f"ССП {not_found_ids} не найдены")
|
||||||
|
|
||||||
new_forms = await bf_service.bulk_create_if_not_exists(
|
new_forms = await bf_service.bulk_create(
|
||||||
form_type_code=form.form_type_code,
|
form_type_code=form.form_type_code,
|
||||||
year=form.year,
|
year=form.year,
|
||||||
org_unit_ids=org_unit_ids,
|
org_unit_ids=org_unit_ids,
|
||||||
@ -449,18 +384,6 @@ async def add_form(
|
|||||||
|
|
||||||
db_ms = (time.perf_counter() - t0) * 1000
|
db_ms = (time.perf_counter() - t0) * 1000
|
||||||
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
||||||
extra={
|
|
||||||
"type": "db_time",
|
|
||||||
"time_ms": f"{db_ms:.2f}",
|
|
||||||
"handler": "add_form",
|
|
||||||
"form_type_code": form.form_type_code,
|
|
||||||
"user": current_user.id,
|
|
||||||
"count_orgs": len(form.org_unit_ids),
|
|
||||||
}
|
|
||||||
logger.info(
|
|
||||||
f"db_time: {extra}",
|
|
||||||
extra=extra,
|
|
||||||
)
|
|
||||||
|
|
||||||
return BaseListResponse(
|
return BaseListResponse(
|
||||||
result=[BudgetFormResponse.model_validate(new_form) for new_form in new_forms],
|
result=[BudgetFormResponse.model_validate(new_form) for new_form in new_forms],
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from src.core.errors import AccessDeniedException
|
|||||||
from src.api.v1.deps import get_current_active_user, require_admin
|
from src.api.v1.deps import get_current_active_user, require_admin
|
||||||
from src.db.models.app_user import AppUser
|
from src.db.models.app_user import AppUser
|
||||||
from src.db.session import get_db
|
from src.db.session import get_db
|
||||||
from src.domain.schemas import BaseListResponse, BaseSingleResponse, OrgUnitCreateSchema, OrgUnitListSchema, OrgUnitSchema, OrgUnitUpdateSchema, ResponseBase
|
from src.domain.schemas import BaseListResponse, BaseSingleResponse, OrgUnitBaseSchema, OrgUnitListSchema, OrgUnitSchema, OrgUnitUpdateSchema, ResponseBase
|
||||||
from src.services.org_unit_service import OrgUnitService
|
from src.services.org_unit_service import OrgUnitService
|
||||||
|
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ async def org_unit_id(
|
|||||||
|
|
||||||
@router.post("/")
|
@router.post("/")
|
||||||
async def create_ssp(
|
async def create_ssp(
|
||||||
org_data: OrgUnitCreateSchema,
|
org_data: OrgUnitBaseSchema,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: AppUser = Depends(require_admin),
|
current_user: AppUser = Depends(require_admin),
|
||||||
) -> BaseSingleResponse[OrgUnitSchema]:
|
) -> BaseSingleResponse[OrgUnitSchema]:
|
||||||
@ -75,7 +75,6 @@ async def create_ssp(
|
|||||||
org_unit = await org_unit_service.create(
|
org_unit = await org_unit_service.create(
|
||||||
title=org_data.title,
|
title=org_data.title,
|
||||||
is_ssp=org_data.is_ssp,
|
is_ssp=org_data.is_ssp,
|
||||||
utc_offset=org_data.utc_offset,
|
|
||||||
user=current_user,
|
user=current_user,
|
||||||
)
|
)
|
||||||
return BaseSingleResponse(
|
return BaseSingleResponse(
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import logging
|
|
||||||
import time
|
import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@ -21,9 +20,6 @@ from src.domain.schemas import (
|
|||||||
from src.services.project_service import ProjectService
|
from src.services.project_service import ProjectService
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(tags=["forms"])
|
router = APIRouter(tags=["forms"])
|
||||||
FORM3_ALLOWED_SECTIONS = {"q1", "q2", "q3", "q4", "year"}
|
FORM3_ALLOWED_SECTIONS = {"q1", "q2", "q3", "q4", "year"}
|
||||||
|
|
||||||
@ -100,20 +96,6 @@ async def get_project_report(
|
|||||||
)
|
)
|
||||||
db_ms = (time.perf_counter() - t0) * 1000
|
db_ms = (time.perf_counter() - t0) * 1000
|
||||||
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
||||||
extra={
|
|
||||||
"type": "db_time",
|
|
||||||
"time_ms": f"{db_ms:.2f}",
|
|
||||||
"handler": "get_project_report",
|
|
||||||
"project_id": project_id,
|
|
||||||
"year": year,
|
|
||||||
"report_type": report_type,
|
|
||||||
"sections": sections,
|
|
||||||
"user": current_user.id,
|
|
||||||
}
|
|
||||||
logger.info(
|
|
||||||
f"db_time: {extra}",
|
|
||||||
extra=extra,
|
|
||||||
)
|
|
||||||
return BaseListResponse(count=len(rows), result=_rows_to_payload(rows))
|
return BaseListResponse(count=len(rows), result=_rows_to_payload(rows))
|
||||||
|
|
||||||
|
|
||||||
@ -136,19 +118,6 @@ async def get_rf_rollup(
|
|||||||
)
|
)
|
||||||
db_ms = (time.perf_counter() - t0) * 1000
|
db_ms = (time.perf_counter() - t0) * 1000
|
||||||
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
||||||
extra={
|
|
||||||
"type": "db_time",
|
|
||||||
"time_ms": f"{db_ms:.2f}",
|
|
||||||
"handler": "get_project_report",
|
|
||||||
"branch_id": branch_id,
|
|
||||||
"year": year,
|
|
||||||
"sections": sections,
|
|
||||||
"user": current_user.id,
|
|
||||||
}
|
|
||||||
logger.info(
|
|
||||||
f"db_time: {extra}",
|
|
||||||
extra=extra,
|
|
||||||
)
|
|
||||||
return BaseListResponse(count=len(rows), result=_rows_to_payload(rows))
|
return BaseListResponse(count=len(rows), result=_rows_to_payload(rows))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -27,9 +27,9 @@ async def get_vsp(
|
|||||||
ssp_id: int | None = None,
|
ssp_id: int | None = None,
|
||||||
open_date_start: date | None = None,
|
open_date_start: date | None = None,
|
||||||
open_date_end: date | None = None,
|
open_date_end: date | None = None,
|
||||||
placement_type: str | None = None,
|
location_form: str | None = None,
|
||||||
staff_count_min: int | None = None,
|
numbers_min: int | None = None,
|
||||||
staff_count_max: int | None = None,
|
numbers_max: int | None = None,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: AppUser = Depends(require_executor),
|
current_user: AppUser = Depends(require_executor),
|
||||||
):
|
):
|
||||||
@ -42,9 +42,9 @@ async def get_vsp(
|
|||||||
ssp_id=ssp_id,
|
ssp_id=ssp_id,
|
||||||
open_date_start=open_date_start,
|
open_date_start=open_date_start,
|
||||||
open_date_end=open_date_end,
|
open_date_end=open_date_end,
|
||||||
placement_type=placement_type,
|
location_form=location_form,
|
||||||
staff_count_min=staff_count_min,
|
numbers_min=numbers_min,
|
||||||
staff_count_max=staff_count_max,
|
numbers_max=numbers_max,
|
||||||
load_org_unit=True,
|
load_org_unit=True,
|
||||||
with_count=True,
|
with_count=True,
|
||||||
)
|
)
|
||||||
@ -89,9 +89,9 @@ async def export_info(
|
|||||||
ssp_ids=body.ssp_ids,
|
ssp_ids=body.ssp_ids,
|
||||||
open_date_start=body.open_date_start,
|
open_date_start=body.open_date_start,
|
||||||
open_date_end=body.open_date_end,
|
open_date_end=body.open_date_end,
|
||||||
placement_type=body.placement_type,
|
location_form=body.location_form,
|
||||||
staff_count_min=body.staff_count_min,
|
numbers_min=body.numbers_min,
|
||||||
staff_count_max=body.staff_count_max,
|
numbers_max=body.numbers_max,
|
||||||
)
|
)
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
stream,
|
stream,
|
||||||
|
|||||||
@ -4,8 +4,6 @@ from dataclasses import asdict
|
|||||||
import dataclasses
|
import dataclasses
|
||||||
import enum
|
import enum
|
||||||
from json import dumps, loads
|
from json import dumps, loads
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -14,9 +12,6 @@ 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_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
|
||||||
@ -31,9 +26,6 @@ from src.db.session import SessionLocal
|
|||||||
from src.services.user_service import UserService
|
from src.services.user_service import UserService
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def get_db_session(user_id: int | None = None):
|
async def get_db_session(user_id: int | None = None):
|
||||||
"""Контекстный менеджер для получения сессии базы данных."""
|
"""Контекстный менеджер для получения сессии базы данных."""
|
||||||
@ -273,7 +265,7 @@ class ConnectionManager:
|
|||||||
for lock_key in lock_keys:
|
for lock_key in lock_keys:
|
||||||
self.cell_locks.pop(lock_key, None)
|
self.cell_locks.pop(lock_key, None)
|
||||||
|
|
||||||
def is_cell_available_for_edit(
|
def is_locked_by_other(
|
||||||
self,
|
self,
|
||||||
con_key: ConnectionKeyEnum,
|
con_key: ConnectionKeyEnum,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
@ -281,10 +273,10 @@ class ConnectionManager:
|
|||||||
form_key,
|
form_key,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not cell_key:
|
if not cell_key:
|
||||||
return True
|
return False
|
||||||
cell_key = frozenset(cell_key.items())
|
cell_key = frozenset(cell_key.items())
|
||||||
lock_owner_id = self.cell_locks.get((con_key, frozenset(form_key.items()), cell_key))
|
lock_owner_id = self.cell_locks.get((con_key, frozenset(form_key.items()), cell_key))
|
||||||
return lock_owner_id is not None and lock_owner_id == user_id
|
return lock_owner_id is not None and lock_owner_id != user_id
|
||||||
|
|
||||||
def is_row_locked_by_other(
|
def is_row_locked_by_other(
|
||||||
self,
|
self,
|
||||||
@ -377,18 +369,6 @@ 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
|
||||||
|
|
||||||
@ -475,41 +455,6 @@ class FormEventProcess:
|
|||||||
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,
|
||||||
@ -540,8 +485,7 @@ 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.prl_service: RfProjectReportLineService = RfProjectReportLineService(db)
|
self.bf_service: BudgetFormService = BudgetFormService(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(
|
||||||
@ -624,16 +568,11 @@ class ProjectEventProcess:
|
|||||||
expense_item_id: Optional[int] = None
|
expense_item_id: Optional[int] = None
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
project_report = await self.pr_service.get(
|
project = await self.project_service.get(project_id=project_id, user=user)
|
||||||
year=year,
|
if not project:
|
||||||
report_type=report_type,
|
|
||||||
project_id=project_id,
|
|
||||||
)
|
|
||||||
if not project_report:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
ids = await self.prl_service.get_ids(project_report_id=project_report.id)
|
return await self.project_service.add_form3_line(
|
||||||
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,
|
||||||
@ -641,16 +580,6 @@ 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,
|
||||||
@ -661,7 +590,7 @@ class ProjectEventProcess:
|
|||||||
) -> list[tuple]:
|
) -> list[tuple]:
|
||||||
"""
|
"""
|
||||||
data = {
|
data = {
|
||||||
"row_id": int,
|
"line_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)
|
||||||
@ -672,7 +601,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["row_id"],
|
line_id=event_data["line_id"],
|
||||||
user=user,
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -710,7 +639,7 @@ def resolve_cell_key(event_data: dict) -> dict | None:
|
|||||||
if not data or "line_id" not in data or "column" not in data:
|
if not data or "line_id" not in data or "column" not in data:
|
||||||
return None
|
return None
|
||||||
return {
|
return {
|
||||||
"line_id": data.get("line_id_code", data["line_id"]),
|
"line_id": data["line_id"],
|
||||||
"column": data["column"],
|
"column": data["column"],
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -798,7 +727,7 @@ async def process_websocket(
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
case "cell_updated":
|
case "cell_updated":
|
||||||
if not manager.is_cell_available_for_edit(
|
if manager.is_locked_by_other(
|
||||||
con_key=con_key,
|
con_key=con_key,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
cell_key=cell_key,
|
cell_key=cell_key,
|
||||||
@ -810,18 +739,18 @@ async def process_websocket(
|
|||||||
websocket,
|
websocket,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
# if cell_key and not manager.acquire_cell_lock(
|
if cell_key and not manager.acquire_cell_lock(
|
||||||
# con_key=con_key,
|
con_key=con_key,
|
||||||
# user_id=user_id,
|
user_id=user_id,
|
||||||
# cell_key=cell_key,
|
cell_key=cell_key,
|
||||||
# form_key=kwargs_process,
|
form_key=kwargs_process,
|
||||||
# ):
|
):
|
||||||
# data["error"] = "Ячейка уже редактируется другим пользователем"
|
data["error"] = "Ячейка уже редактируется другим пользователем"
|
||||||
# await manager.send_back(
|
await manager.send_back(
|
||||||
# data,
|
data,
|
||||||
# websocket,
|
websocket,
|
||||||
# )
|
)
|
||||||
# continue
|
continue
|
||||||
case "row_deleted":
|
case "row_deleted":
|
||||||
if manager.is_row_locked_by_other(
|
if manager.is_row_locked_by_other(
|
||||||
con_key=con_key,
|
con_key=con_key,
|
||||||
@ -841,26 +770,12 @@ 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()
|
|
||||||
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,
|
||||||
**kwargs_process
|
**kwargs_process
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
db_ms = (time.perf_counter() - t0) * 1000
|
|
||||||
|
|
||||||
extra = {
|
|
||||||
"type": "db_time",
|
|
||||||
"time_ms": f"{db_ms:.2f}",
|
|
||||||
"event": data.get("event"),
|
|
||||||
}
|
|
||||||
extra.update(data_old)
|
|
||||||
logger.info(
|
|
||||||
f"db_time: {extra}",
|
|
||||||
extra=extra,
|
|
||||||
)
|
|
||||||
|
|
||||||
if data.get("event") == "row_deleted":
|
if data.get("event") == "row_deleted":
|
||||||
manager.release_locks_for_row(
|
manager.release_locks_for_row(
|
||||||
@ -872,8 +787,6 @@ 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(
|
||||||
@ -902,12 +815,6 @@ 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)
|
||||||
|
|||||||
@ -9,9 +9,7 @@ from src.db.models.ckk import Ckk
|
|||||||
from src.db.models.collegial_approval import CollegialApproval
|
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, ExpenseItemFormType
|
from src.db.models.expense_item import ExpenseItem
|
||||||
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
|
||||||
|
|||||||
@ -25,12 +25,3 @@ class ExpenseItem(Base):
|
|||||||
direction: Mapped[str | None] = mapped_column(String)
|
direction: Mapped[str | None] = mapped_column(String)
|
||||||
parent_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("v3.expense_item.id"))
|
parent_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("v3.expense_item.id"))
|
||||||
depth: Mapped[int | None] = mapped_column(Integer, default=0)
|
depth: Mapped[int | None] = mapped_column(Integer, default=0)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ExpenseItemFormType(Base):
|
|
||||||
__tablename__ = "expense_item_form_type"
|
|
||||||
__table_args__ = {"schema": "v3"}
|
|
||||||
|
|
||||||
form_type_code: Mapped[str] = mapped_column(String, primary_key=True)
|
|
||||||
expense_item_id: Mapped[int] = mapped_column(Integer, ForeignKey("v3.expense_item.id"), primary_key=True)
|
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
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))
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
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
|
|
||||||
@ -21,7 +21,6 @@ class OrgUnit(Base):
|
|||||||
title: Mapped[str] = mapped_column(String)
|
title: Mapped[str] = mapped_column(String)
|
||||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
is_ssp: Mapped[bool] = mapped_column(Boolean)
|
is_ssp: Mapped[bool] = mapped_column(Boolean)
|
||||||
utc_offset: Mapped[str] = mapped_column(String(3), default="+03")
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def users_count(self) -> int | None:
|
def users_count(self) -> int | None:
|
||||||
|
|||||||
@ -22,8 +22,8 @@ class PhaseTemplate(Base):
|
|||||||
phase_code: Mapped[str] = mapped_column(String, primary_key=True)
|
phase_code: Mapped[str] = mapped_column(String, primary_key=True)
|
||||||
role: Mapped[str] = mapped_column(String, ForeignKey("v3.role.code"))
|
role: Mapped[str] = mapped_column(String, ForeignKey("v3.role.code"))
|
||||||
column_keys: Mapped[list[str]] = mapped_column(ARRAY(Text))
|
column_keys: Mapped[list[str]] = mapped_column(ARRAY(Text))
|
||||||
opens_at: Mapped[datetime] = mapped_column(DateTime(timezone=False))
|
opens_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||||
closes_at: Mapped[datetime] = mapped_column(DateTime(timezone=False))
|
closes_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||||
|
|
||||||
# form_type_rel = relationship("FormType", foreign_keys=[form_type])
|
# form_type_rel = relationship("FormType", foreign_keys=[form_type])
|
||||||
# role_rel = relationship("Role", foreign_keys=[role], back_populates="phase_templates")
|
# role_rel = relationship("Role", foreign_keys=[role], back_populates="phase_templates")
|
||||||
|
|||||||
@ -46,4 +46,4 @@ class Project(Base):
|
|||||||
# children: Mapped[list["Project"]] = relationship("Project", back_populates="parent") # type: ignore
|
# children: Mapped[list["Project"]] = relationship("Project", back_populates="parent") # type: ignore
|
||||||
# org_unit = relationship("OrgUnit", back_populates="projects")
|
# org_unit = relationship("OrgUnit", back_populates="projects")
|
||||||
# budget_lines: Mapped[list["BudgetLine"]] = relationship("BudgetLine", back_populates="project") # type: ignore
|
# budget_lines: Mapped[list["BudgetLine"]] = relationship("BudgetLine", back_populates="project") # type: ignore
|
||||||
rf_project_reports: Mapped[list["RfProjectReport"]] = relationship("RfProjectReport", back_populates="project") # type: ignore
|
# rf_project_reports: Mapped[list["RfProjectReport"]] = relationship("RfProjectReport", back_populates="project") # type: ignore
|
||||||
|
|||||||
@ -25,7 +25,7 @@ class RfProjectReport(Base):
|
|||||||
updated_by: Mapped[int | None] = mapped_column(Integer, ForeignKey("v3.app_user.id"))
|
updated_by: Mapped[int | None] = mapped_column(Integer, ForeignKey("v3.app_user.id"))
|
||||||
updated_at: Mapped[datetime | None] = mapped_column(DateTime, server_default=func.now())
|
updated_at: Mapped[datetime | None] = mapped_column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
project: Mapped["Project"] = relationship("Project", back_populates="rf_project_reports") # type: ignore
|
# project: Mapped[Project] = relationship("Project", back_populates="rf_project_reports")
|
||||||
# creator: Mapped[AppUser | None] = relationship("AppUser", foreign_keys=[created_by])
|
# creator: Mapped[AppUser | None] = relationship("AppUser", foreign_keys=[created_by])
|
||||||
# updater: Mapped[AppUser | None] = relationship("AppUser", foreign_keys=[updated_by])
|
# updater: Mapped[AppUser | None] = relationship("AppUser", foreign_keys=[updated_by])
|
||||||
# lines: Mapped[list[RfProjectReportLine]] = relationship("RfProjectReportLine", back_populates="report")
|
# lines: Mapped[list[RfProjectReportLine]] = relationship("RfProjectReportLine", back_populates="report")
|
||||||
|
|||||||
@ -45,6 +45,8 @@ class Vsp(Base):
|
|||||||
)
|
)
|
||||||
vsp_type: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
vsp_type: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||||
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
location_form: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||||
|
numbers: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||||
rent_contract_num: Mapped[str | None] = mapped_column(String, nullable=True)
|
rent_contract_num: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||||
rent_end_date : Mapped[date | None]= mapped_column(Date, nullable=True)
|
rent_end_date : Mapped[date | None]= mapped_column(Date, nullable=True)
|
||||||
|
|
||||||
|
|||||||
@ -233,16 +233,10 @@ class OrgUnitBaseSchema(BaseModel):
|
|||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
class OrgUnitCreateSchema(OrgUnitBaseSchema):
|
|
||||||
title: str = Field(..., min_length=1)
|
|
||||||
utc_offset: str = Field('+03', min_length=3, pattern=r'^[+]\d\d$')
|
|
||||||
|
|
||||||
|
|
||||||
class OrgUnitUpdateSchema(OrgUnitBaseSchema):
|
class OrgUnitUpdateSchema(OrgUnitBaseSchema):
|
||||||
title: Optional[str] = None
|
title: Optional[str] = None
|
||||||
is_ssp: Optional[bool] = None
|
is_ssp: Optional[bool] = None
|
||||||
is_active: Optional[bool] = None
|
is_active: Optional[bool] = None
|
||||||
utc_offset: Optional[str] = Field('+03', min_length=3, pattern=r'^[+]\d\d$')
|
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
@ -250,7 +244,6 @@ class OrgUnitUpdateSchema(OrgUnitBaseSchema):
|
|||||||
class OrgUnitResponseSchema(OrgUnitBaseSchema):
|
class OrgUnitResponseSchema(OrgUnitBaseSchema):
|
||||||
id: int
|
id: int
|
||||||
is_active: bool
|
is_active: bool
|
||||||
utc_offset: str
|
|
||||||
users_count: int | None = None
|
users_count: int | None = None
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
@ -268,7 +261,6 @@ class OrgUnitResponseSchema(OrgUnitBaseSchema):
|
|||||||
"is_ssp": data.is_ssp,
|
"is_ssp": data.is_ssp,
|
||||||
"is_ssp": data.is_ssp,
|
"is_ssp": data.is_ssp,
|
||||||
"is_active": data.is_active,
|
"is_active": data.is_active,
|
||||||
"utc_offset": data.utc_offset
|
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result["users_count"] = data.users_count
|
result["users_count"] = data.users_count
|
||||||
@ -366,7 +358,7 @@ class AddProjectBody(BaseModel):
|
|||||||
max_length=30,
|
max_length=30,
|
||||||
pattern=r"^[^+\-\/\\=&*\s]{1,30}$",
|
pattern=r"^[^+\-\/\\=&*\s]{1,30}$",
|
||||||
)
|
)
|
||||||
year: int = Field(default_factory=lambda: datetime.now().year)
|
year: int
|
||||||
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
|
||||||
@ -378,10 +370,6 @@ class AddProjectBody(BaseModel):
|
|||||||
staff_count: Optional[int] = None
|
staff_count: Optional[int] = None
|
||||||
total_area: Optional[float] = None
|
total_area: Optional[float] = None
|
||||||
|
|
||||||
@field_validator("year", mode="before")
|
|
||||||
@classmethod
|
|
||||||
def optional_year(cls, v: Any) -> Any:
|
|
||||||
return v or datetime.now().year
|
|
||||||
|
|
||||||
class AddLineSchema(BaseModel):
|
class AddLineSchema(BaseModel):
|
||||||
expense_item_id: Optional[int] = None
|
expense_item_id: Optional[int] = None
|
||||||
@ -425,35 +413,6 @@ class FormPhaseUpdate(BaseModel):
|
|||||||
closes_at: datetime | None = None
|
closes_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
class Form3PhaseResponse(BaseModel):
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
|
||||||
|
|
||||||
phase_code: str
|
|
||||||
role: str
|
|
||||||
column_keys: list[str]
|
|
||||||
opens_at: datetime
|
|
||||||
closes_at: datetime
|
|
||||||
year: int | None = None
|
|
||||||
report_type: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class Form3PhaseCreate(BaseModel):
|
|
||||||
year: int
|
|
||||||
report_type: str
|
|
||||||
phase_code: str
|
|
||||||
role: FormPhaseRole
|
|
||||||
column_keys: list[str]
|
|
||||||
opens_at: datetime
|
|
||||||
closes_at: datetime
|
|
||||||
|
|
||||||
|
|
||||||
class Form3PhaseUpdate(BaseModel):
|
|
||||||
role: FormPhaseRole | None = None
|
|
||||||
column_keys: list[str] | None = None
|
|
||||||
opens_at: datetime | None = None
|
|
||||||
closes_at: datetime | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class AuditLogBase(BaseModel):
|
class AuditLogBase(BaseModel):
|
||||||
"""Базовая схема записи аудита (единый формат вывода как у auditlog)."""
|
"""Базовая схема записи аудита (единый формат вывода как у auditlog)."""
|
||||||
|
|
||||||
@ -531,8 +490,8 @@ class VSPBase(BaseModel):
|
|||||||
serialization_alias='approved_format',
|
serialization_alias='approved_format',
|
||||||
)
|
)
|
||||||
notes: Optional[str] = Field(None, description="Примечания")
|
notes: Optional[str] = Field(None, description="Примечания")
|
||||||
placement_type: Optional[str] = Field(None, description="Тип размещения")
|
location_form: Optional[str] = Field(None, description="Форма расположения")
|
||||||
staff_count: Optional[int] = Field(None, description="Штатная численность")
|
numbers: Optional[int] = Field(None, description="Штатная численность")
|
||||||
area: Optional[float] = Field(
|
area: Optional[float] = Field(
|
||||||
None,
|
None,
|
||||||
description="Арендная площадь",
|
description="Арендная площадь",
|
||||||
@ -580,8 +539,8 @@ class VSPEditBase(BaseModel):
|
|||||||
serialization_alias='format',
|
serialization_alias='format',
|
||||||
)
|
)
|
||||||
notes: Optional[str] = Field(None, description="Примечания")
|
notes: Optional[str] = Field(None, description="Примечания")
|
||||||
placement_type: Optional[str] = Field(None, description="Тип размещения")
|
location_form: Optional[str] = Field(None, description="Форма расположения")
|
||||||
staff_count: Optional[int] = Field(None, description="Штатная численность")
|
numbers: Optional[int] = Field(None, description="Штатная численность")
|
||||||
total_area: Optional[float] = Field(
|
total_area: Optional[float] = Field(
|
||||||
None,
|
None,
|
||||||
description="Арендная площадь",
|
description="Арендная площадь",
|
||||||
@ -652,9 +611,9 @@ class VSPExportRequest(BaseModel):
|
|||||||
ssp_ids: Optional[List[int]] = None
|
ssp_ids: Optional[List[int]] = None
|
||||||
open_date_start: Optional[date] = None
|
open_date_start: Optional[date] = None
|
||||||
open_date_end: Optional[date] = None
|
open_date_end: Optional[date] = None
|
||||||
placement_type: Optional[str] = None
|
location_form: Optional[str] = None
|
||||||
staff_count_min: Optional[int] = None
|
numbers_min: Optional[int] = None
|
||||||
staff_count_max: Optional[int] = None
|
numbers_max: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class ExpenseItemBaseSchema(BaseModel):
|
class ExpenseItemBaseSchema(BaseModel):
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@ -21,10 +20,6 @@ from src.core.exception_handlers import register_exception_handlers
|
|||||||
from src.db.base import engine
|
from src.db.base import engine
|
||||||
from src.db.session import create_tables
|
from src.db.session import create_tables
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
|
|
||||||
# if not settings.DEBUG:
|
# if not settings.DEBUG:
|
||||||
# from raisa_fastapi_protected_api import (
|
# from raisa_fastapi_protected_api import (
|
||||||
# AuthorizationMiddleware,
|
# AuthorizationMiddleware,
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
from sqlalchemy import func, select, text
|
from sqlalchemy import func, select, text
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.db.models.form_type import FormTypeEnum
|
|
||||||
from src.db.models.budget_form import BudgetForm
|
from src.db.models.budget_form import BudgetForm
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
@ -59,35 +58,20 @@ class BudgetFormRepository:
|
|||||||
|
|
||||||
async def get(
|
async def get(
|
||||||
self,
|
self,
|
||||||
budget_form_id: int | None = None,
|
budget_form_id: int,
|
||||||
org_unit: int | list[int] | None = None,
|
org_unit: int | list[int] | None = None,
|
||||||
form_type_code: FormTypeEnum | str | None = None,
|
|
||||||
year: int | None = None,
|
|
||||||
load_form_type: bool = False,
|
load_form_type: bool = False,
|
||||||
load_org: bool = False,
|
load_org: bool = False,
|
||||||
) -> BudgetForm | None:
|
) -> BudgetForm | None:
|
||||||
assert budget_form_id or all((form_type_code, year, org_unit))
|
|
||||||
query = select(BudgetForm)
|
query = select(BudgetForm)
|
||||||
|
|
||||||
where = []
|
where = [BudgetForm.id == budget_form_id]
|
||||||
if budget_form_id is not None:
|
|
||||||
where.append(BudgetForm.id == budget_form_id)
|
|
||||||
|
|
||||||
if org_unit is not None:
|
if org_unit is not None:
|
||||||
if isinstance(org_unit, int):
|
if isinstance(org_unit, int):
|
||||||
where.append(BudgetForm.org_unit_id == org_unit)
|
where.append(BudgetForm.org_unit_id == org_unit)
|
||||||
else:
|
else:
|
||||||
where.append(BudgetForm.org_unit_id.in_(org_unit))
|
where.append(BudgetForm.org_unit_id.in_(org_unit))
|
||||||
|
|
||||||
if form_type_code is not None:
|
|
||||||
if isinstance(form_type_code, FormTypeEnum):
|
|
||||||
where.append(BudgetForm.form_type_code == form_type_code.value)
|
|
||||||
else:
|
|
||||||
where.append(BudgetForm.form_type_code == form_type_code)
|
|
||||||
|
|
||||||
if year is not None:
|
|
||||||
where.append(BudgetForm.year == year)
|
|
||||||
|
|
||||||
query = query.where(*where)
|
query = query.where(*where)
|
||||||
if load_org:
|
if load_org:
|
||||||
query = query.options(
|
query = query.options(
|
||||||
|
|||||||
@ -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]) -> list[BudgetLine]:
|
async def get_list(self, budget_line_ids: list[int]) -> BudgetLine | None:
|
||||||
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()
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.db.models.form_type import FormTypeEnum
|
from src.db.models.expense_item import ExpenseItem
|
||||||
from src.db.models.expense_item import ExpenseItem, ExpenseItemFormType
|
|
||||||
|
|
||||||
|
|
||||||
class ExpenseItemRepository:
|
class ExpenseItemRepository:
|
||||||
@ -12,32 +11,3 @@ class ExpenseItemRepository:
|
|||||||
async def get(self, item_id: int) -> ExpenseItem | None:
|
async def get(self, item_id: int) -> ExpenseItem | None:
|
||||||
query = select(ExpenseItem).where(ExpenseItem.id == item_id).limit(1)
|
query = select(ExpenseItem).where(ExpenseItem.id == item_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,
|
|
||||||
r_start: bool | None = None,
|
|
||||||
sheet: str | None = None,
|
|
||||||
direction: str | None = None,
|
|
||||||
form_type: FormTypeEnum | str | None = None,
|
|
||||||
) -> list[ExpenseItem]:
|
|
||||||
where = []
|
|
||||||
query = select(ExpenseItem)
|
|
||||||
if form_type is not None:
|
|
||||||
query = query.join(
|
|
||||||
ExpenseItemFormType,
|
|
||||||
ExpenseItemFormType.expense_item_id == ExpenseItem.id,
|
|
||||||
).where(
|
|
||||||
ExpenseItemFormType.form_type_code == (form_type.value if isinstance(form_type, FormTypeEnum) else form_type)
|
|
||||||
)
|
|
||||||
if r_start is not None:
|
|
||||||
where.append(ExpenseItem.item_id.startswith("R"))
|
|
||||||
if sheet is not None:
|
|
||||||
where.append(ExpenseItem.sheet == sheet)
|
|
||||||
if direction is not None:
|
|
||||||
where.append(ExpenseItem.direction == direction)
|
|
||||||
query = query.where(*where)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (await self.db.execute(query)).scalars().all()
|
|
||||||
|
|||||||
@ -1,116 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from sqlalchemy import select, text
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
from src.db.models.form3_phase import Form3Phase
|
|
||||||
|
|
||||||
|
|
||||||
class Form3PhaseRepository:
|
|
||||||
def __init__(self, db: AsyncSession):
|
|
||||||
self.db = db
|
|
||||||
|
|
||||||
async def get_list(
|
|
||||||
self,
|
|
||||||
report_id: int | list[int],
|
|
||||||
phase_code: str | None = None,
|
|
||||||
) -> list[Form3Phase]:
|
|
||||||
if isinstance(report_id, int):
|
|
||||||
query = select(Form3Phase).where(Form3Phase.rf_project_report_id == report_id)
|
|
||||||
else:
|
|
||||||
query = select(Form3Phase).where(Form3Phase.rf_project_report_id.in_(report_id))
|
|
||||||
if phase_code is not None:
|
|
||||||
query = query.where(Form3Phase.phase_code == phase_code)
|
|
||||||
return (await self.db.execute(query)).scalars().all()
|
|
||||||
|
|
||||||
async def get(
|
|
||||||
self, report_id: int, phase_code: str
|
|
||||||
) -> Form3Phase | None:
|
|
||||||
query = (
|
|
||||||
select(Form3Phase)
|
|
||||||
.where(Form3Phase.rf_project_report_id == report_id)
|
|
||||||
.where(Form3Phase.phase_code == phase_code)
|
|
||||||
.limit(1)
|
|
||||||
)
|
|
||||||
return (await self.db.execute(query)).scalar_one_or_none()
|
|
||||||
|
|
||||||
async def create(
|
|
||||||
self,
|
|
||||||
report_id: int,
|
|
||||||
phase_code: str,
|
|
||||||
role: str,
|
|
||||||
column_keys: list[str],
|
|
||||||
opens_at: datetime,
|
|
||||||
closes_at: datetime,
|
|
||||||
) -> Form3Phase:
|
|
||||||
result = await self.db.execute(
|
|
||||||
text(
|
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
(v3.add_form3_phase(
|
|
||||||
:report_id,
|
|
||||||
:phase_code,
|
|
||||||
:role,
|
|
||||||
:column_keys,
|
|
||||||
CAST(:opens_at AS TIMESTAMP WITHOUT TIME ZONE),
|
|
||||||
CAST(:closes_at AS TIMESTAMP WITHOUT TIME ZONE)
|
|
||||||
)).rf_project_report_id
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
{
|
|
||||||
"report_id": report_id,
|
|
||||||
"phase_code": phase_code,
|
|
||||||
"role": role,
|
|
||||||
"column_keys": column_keys,
|
|
||||||
"opens_at": opens_at,
|
|
||||||
"closes_at": closes_at,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if result.scalar_one_or_none() is None:
|
|
||||||
return None
|
|
||||||
return await self.get(report_id, phase_code)
|
|
||||||
|
|
||||||
async def update(
|
|
||||||
self, report_id: int, phase_code: str, data: dict
|
|
||||||
) -> Form3Phase | None:
|
|
||||||
result = await self.db.execute(
|
|
||||||
text(
|
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
(v3.upd_form3_phase(
|
|
||||||
:report_id,
|
|
||||||
:phase_code,
|
|
||||||
:role,
|
|
||||||
:column_keys,
|
|
||||||
CAST(:opens_at AS TIMESTAMP WITHOUT TIME ZONE),
|
|
||||||
CAST(:closes_at AS TIMESTAMP WITHOUT TIME ZONE)
|
|
||||||
)).rf_project_report_id
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
{
|
|
||||||
"report_id": report_id,
|
|
||||||
"phase_code": phase_code,
|
|
||||||
"role": data.get("role"),
|
|
||||||
"column_keys": data.get("column_keys"),
|
|
||||||
"opens_at": data.get("opens_at"),
|
|
||||||
"closes_at": data.get("closes_at"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if result.scalar_one_or_none() is None:
|
|
||||||
return None
|
|
||||||
return await self.get(report_id, phase_code)
|
|
||||||
|
|
||||||
async def delete(
|
|
||||||
self, report_id: int, phase_code: str
|
|
||||||
) -> bool:
|
|
||||||
result = await self.db.execute(
|
|
||||||
text(
|
|
||||||
"SELECT v3.del_form3_phase(:report_id, :phase_code)"
|
|
||||||
),
|
|
||||||
{
|
|
||||||
"report_id": report_id,
|
|
||||||
"phase_code": phase_code,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
deleted = result.scalar_one_or_none()
|
|
||||||
return bool(deleted)
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
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()
|
|
||||||
@ -55,8 +55,8 @@ class FormPhaseRepository:
|
|||||||
:phase_code,
|
:phase_code,
|
||||||
:role,
|
:role,
|
||||||
:column_keys,
|
:column_keys,
|
||||||
CAST(:opens_at AS TIMESTAMP WITHOUT TIME ZONE),
|
:opens_at,
|
||||||
CAST(:closes_at AS TIMESTAMP WITHOUT TIME ZONE)
|
:closes_at
|
||||||
)).budget_form_id
|
)).budget_form_id
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
@ -87,8 +87,8 @@ class FormPhaseRepository:
|
|||||||
:phase_code,
|
:phase_code,
|
||||||
:role,
|
:role,
|
||||||
:column_keys,
|
:column_keys,
|
||||||
CAST(:opens_at AS TIMESTAMP WITHOUT TIME ZONE),
|
:opens_at,
|
||||||
CAST(:closes_at AS TIMESTAMP WITHOUT TIME ZONE)
|
:closes_at
|
||||||
)).budget_form_id
|
)).budget_form_id
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
from sqlalchemy import func, select, text, update
|
from sqlalchemy import func, select, update
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.db.models.org_unit import OrgUnit
|
from src.db.models.org_unit import OrgUnit
|
||||||
@ -84,10 +84,9 @@ class OrgUnitRepository:
|
|||||||
self,
|
self,
|
||||||
title: str,
|
title: str,
|
||||||
is_ssp: bool,
|
is_ssp: bool,
|
||||||
utc_offset: str,
|
|
||||||
) -> OrgUnit:
|
) -> OrgUnit:
|
||||||
"""Создание SSP."""
|
"""Создание SSP."""
|
||||||
ssp = OrgUnit(title=title, is_ssp=is_ssp, utc_offset=utc_offset)
|
ssp = OrgUnit(title=title, is_ssp=is_ssp)
|
||||||
self.db.add(ssp)
|
self.db.add(ssp)
|
||||||
await self.db.flush()
|
await self.db.flush()
|
||||||
# await self.db.refresh(ssp)
|
# await self.db.refresh(ssp)
|
||||||
@ -99,29 +98,10 @@ class OrgUnitRepository:
|
|||||||
if not org_unit:
|
if not org_unit:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
old_offset = org_unit.utc_offset
|
|
||||||
for field, value in kwargs.items():
|
for field, value in kwargs.items():
|
||||||
setattr(org_unit, field, value)
|
setattr(org_unit, field, value)
|
||||||
|
|
||||||
await self.db.flush()
|
await self.db.flush()
|
||||||
|
|
||||||
if org_unit.utc_offset != old_offset:
|
|
||||||
await self.db.execute(
|
|
||||||
text(
|
|
||||||
"""
|
|
||||||
SELECT v3.upd_phases_timezones(
|
|
||||||
:org_unit_id,
|
|
||||||
:old_tz,
|
|
||||||
:new_tz
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
{
|
|
||||||
"org_unit_id": org_unit.id,
|
|
||||||
"old_tz": old_offset,
|
|
||||||
"new_tz": org_unit.utc_offset,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
# await self.db.refresh(ssp)
|
# await self.db.refresh(ssp)
|
||||||
return org_unit
|
return org_unit
|
||||||
|
|
||||||
|
|||||||
@ -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, years: list[InterruptedError] | None = None) -> dict:
|
def _serialize_project(project: Project, org_unit_name: str | None, report_count: int) -> dict:
|
||||||
return {
|
return {
|
||||||
"id": project.id,
|
"id": project.id,
|
||||||
"name": project.name,
|
"name": project.name,
|
||||||
@ -29,7 +29,6 @@ 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
|
||||||
@ -67,19 +66,11 @@ 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)
|
||||||
@ -91,7 +82,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], row[3]) for row in rows]
|
payload = [self._serialize_project(row[0], row[1], row[2]) for row in rows]
|
||||||
if not with_count:
|
if not with_count:
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
@ -110,18 +101,11 @@ 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)
|
||||||
@ -133,24 +117,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], row[3])
|
return self._serialize_project(row[0], row[1], row[2])
|
||||||
|
|
||||||
async def get_instance(
|
|
||||||
self,
|
|
||||||
project_id: int,
|
|
||||||
org_unit_ids: list[int] | None = None,
|
|
||||||
) -> Project | None:
|
|
||||||
query = (
|
|
||||||
select(
|
|
||||||
Project,
|
|
||||||
)
|
|
||||||
.where(Project.id == project_id)
|
|
||||||
)
|
|
||||||
if org_unit_ids is not None:
|
|
||||||
if len(org_unit_ids) == 0:
|
|
||||||
return None
|
|
||||||
query = query.where(Project.org_unit_id.in_(org_unit_ids))
|
|
||||||
return (await self.db.execute(query)).scalar_one_or_none()
|
|
||||||
|
|
||||||
async def get_reports(self, project_id: int) -> list[dict]:
|
async def get_reports(self, project_id: int) -> list[dict]:
|
||||||
line_count_subq = (
|
line_count_subq = (
|
||||||
|
|||||||
@ -1,39 +0,0 @@
|
|||||||
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()
|
|
||||||
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
from sqlalchemy import select
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlalchemy.orm import joinedload
|
|
||||||
|
|
||||||
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,
|
|
||||||
load_project: bool = False,
|
|
||||||
) -> 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
|
|
||||||
)
|
|
||||||
|
|
||||||
if load_project:
|
|
||||||
query = query.options(joinedload(RfProjectReport.project))
|
|
||||||
query = query.limit(1)
|
|
||||||
return (await self.db.execute(query)).scalar_one_or_none()
|
|
||||||
|
|
||||||
async def get_list(
|
|
||||||
self,
|
|
||||||
report_id: int | None = None,
|
|
||||||
year: int | None = None,
|
|
||||||
report_type: str | None = None,
|
|
||||||
project_id: int | None = None,
|
|
||||||
) -> list[RfProjectReport]:
|
|
||||||
query = select(
|
|
||||||
RfProjectReport
|
|
||||||
)
|
|
||||||
if report_id is not None:
|
|
||||||
query = query.where(RfProjectReport.id == report_id)
|
|
||||||
if year is not None:
|
|
||||||
query = query.where(RfProjectReport.year == year)
|
|
||||||
if report_type is not None:
|
|
||||||
query = query.where(RfProjectReport.report_type == report_type)
|
|
||||||
if project_id is not None:
|
|
||||||
query = query.where(RfProjectReport.project_id == project_id)
|
|
||||||
|
|
||||||
return (await self.db.execute(query)).scalars().all()
|
|
||||||
@ -161,59 +161,6 @@ 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,
|
||||||
form_id: int,
|
form_id: int,
|
||||||
|
|||||||
@ -36,9 +36,9 @@ class VSPRepository:
|
|||||||
ssp_ids: list[int] | None = None,
|
ssp_ids: list[int] | None = None,
|
||||||
open_date_start: date | None = None,
|
open_date_start: date | None = None,
|
||||||
open_date_end: date | None = None,
|
open_date_end: date | None = None,
|
||||||
placement_type: str | None = None,
|
location_form: str | None = None,
|
||||||
staff_count_min: int | None = None,
|
numbers_min: int | None = None,
|
||||||
staff_count_max: int | None = None,
|
numbers_max: int | None = None,
|
||||||
is_active: bool | None = True,
|
is_active: bool | None = True,
|
||||||
close_date_is_null: bool | None = None,
|
close_date_is_null: bool | None = None,
|
||||||
load_org_unit: bool = False,
|
load_org_unit: bool = False,
|
||||||
@ -53,11 +53,11 @@ class VSPRepository:
|
|||||||
filters = [
|
filters = [
|
||||||
("registration_number", registration_number),
|
("registration_number", registration_number),
|
||||||
("address", address),
|
("address", address),
|
||||||
("placement_type", placement_type),
|
("location_form", location_form),
|
||||||
("open_date_start", open_date_start),
|
("open_date_start", open_date_start),
|
||||||
("open_date_end", open_date_end),
|
("open_date_end", open_date_end),
|
||||||
("staff_count_min", staff_count_min),
|
("numbers_min", numbers_min),
|
||||||
("staff_count_max", staff_count_max),
|
("numbers_max", numbers_max),
|
||||||
("is_active", is_active),
|
("is_active", is_active),
|
||||||
]
|
]
|
||||||
for filter_name, filter_value in filters:
|
for filter_name, filter_value in filters:
|
||||||
@ -67,16 +67,16 @@ class VSPRepository:
|
|||||||
queries.append(Vsp.reg_number.ilike(f"%{value}%"))
|
queries.append(Vsp.reg_number.ilike(f"%{value}%"))
|
||||||
case ("address", str(value)) if value:
|
case ("address", str(value)) if value:
|
||||||
queries.append(Vsp.address.ilike(f"%{value}%"))
|
queries.append(Vsp.address.ilike(f"%{value}%"))
|
||||||
case ("placement_type", str(value)) if value:
|
case ("location_form", str(value)) if value:
|
||||||
queries.append(Vsp.placement_type.ilike(f"%{value}%"))
|
queries.append(Vsp.location_form.ilike(f"%{value}%"))
|
||||||
case ("open_date_start", value) if value is not None:
|
case ("open_date_start", value) if value is not None:
|
||||||
queries.append(Vsp.opened_at >= value)
|
queries.append(Vsp.opened_at >= value)
|
||||||
case ("open_date_end", value) if value is not None:
|
case ("open_date_end", value) if value is not None:
|
||||||
queries.append(Vsp.opened_at <= value)
|
queries.append(Vsp.opened_at <= value)
|
||||||
case ("staff_count_min", value) if value is not None:
|
case ("numbers_min", value) if value is not None:
|
||||||
queries.append(Vsp.staff_count >= value)
|
queries.append(Vsp.numbers >= value)
|
||||||
case ("staff_count_max", value) if value is not None:
|
case ("numbers_max", value) if value is not None:
|
||||||
queries.append(Vsp.staff_count <= value)
|
queries.append(Vsp.numbers <= value)
|
||||||
case ("is_active", value) if value is not None:
|
case ("is_active", value) if value is not None:
|
||||||
queries.append(Vsp.is_active == value)
|
queries.append(Vsp.is_active == value)
|
||||||
case _:
|
case _:
|
||||||
@ -116,7 +116,6 @@ class VSPRepository:
|
|||||||
|
|
||||||
async def create(self, payload: VSPCreate, created_by: int, load_org_unit: bool = False) -> Vsp:
|
async def create(self, payload: VSPCreate, created_by: int, load_org_unit: bool = False) -> Vsp:
|
||||||
data = payload.model_dump()
|
data = payload.model_dump()
|
||||||
|
|
||||||
vsp_id = (
|
vsp_id = (
|
||||||
await self.db.execute(
|
await self.db.execute(
|
||||||
text("""
|
text("""
|
||||||
@ -125,7 +124,8 @@ class VSPRepository:
|
|||||||
:p_opened_at, :p_placement_type, :p_staff_count,
|
:p_opened_at, :p_placement_type, :p_staff_count,
|
||||||
:p_total_area, :p_closed_at, :p_is_active, :p_is_deleted,
|
:p_total_area, :p_closed_at, :p_is_active, :p_is_deleted,
|
||||||
:p_system_code, :p_created_by, :p_vsp_type, :p_notes,
|
:p_system_code, :p_created_by, :p_vsp_type, :p_notes,
|
||||||
:p_rent_contract_num, :p_rent_end_date
|
:p_location_form, :p_numbers, :p_rent_contract_num,
|
||||||
|
:p_rent_end_date
|
||||||
)
|
)
|
||||||
"""),
|
"""),
|
||||||
{
|
{
|
||||||
@ -144,6 +144,8 @@ class VSPRepository:
|
|||||||
"p_created_by": created_by,
|
"p_created_by": created_by,
|
||||||
"p_vsp_type": data.get("vsp_type"),
|
"p_vsp_type": data.get("vsp_type"),
|
||||||
"p_notes": data.get("notes"),
|
"p_notes": data.get("notes"),
|
||||||
|
"p_location_form": data.get("location_form"),
|
||||||
|
"p_numbers": data.get("numbers"),
|
||||||
"p_rent_contract_num": data.get("rent_contract_num"),
|
"p_rent_contract_num": data.get("rent_contract_num"),
|
||||||
"p_rent_end_date": data.get("rent_end_date"),
|
"p_rent_end_date": data.get("rent_end_date"),
|
||||||
}
|
}
|
||||||
@ -174,9 +176,9 @@ class VSPRepository:
|
|||||||
"opened_at": "p_opened_at",
|
"opened_at": "p_opened_at",
|
||||||
"closed_at": "p_closed_at",
|
"closed_at": "p_closed_at",
|
||||||
"format": "p_format",
|
"format": "p_format",
|
||||||
"placement_type": "p_placement_type",
|
|
||||||
"staff_count": "p_staff_count",
|
|
||||||
"notes": "p_notes",
|
"notes": "p_notes",
|
||||||
|
"location_form": "p_location_form",
|
||||||
|
"numbers": "p_numbers",
|
||||||
"total_area": "p_total_area",
|
"total_area": "p_total_area",
|
||||||
"rent_contract_num": "p_rent_contract_num",
|
"rent_contract_num": "p_rent_contract_num",
|
||||||
"rent_end_date": "p_rent_end_date",
|
"rent_end_date": "p_rent_end_date",
|
||||||
@ -188,7 +190,8 @@ class VSPRepository:
|
|||||||
params = {"p_vsp_id": vsp.id, "p_updated_by": updated_by}
|
params = {"p_vsp_id": vsp.id, "p_updated_by": updated_by}
|
||||||
for field, param in FIELD_TO_PARAM.items():
|
for field, param in FIELD_TO_PARAM.items():
|
||||||
params[param] = data.get(field)
|
params[param] = data.get(field)
|
||||||
|
params.setdefault("p_placement_type", data.get("placement_type"))
|
||||||
|
params.setdefault("p_staff_count", data.get("staff_count"))
|
||||||
await self.db.execute(
|
await self.db.execute(
|
||||||
text("""
|
text("""
|
||||||
SELECT v3.upd_vsp(
|
SELECT v3.upd_vsp(
|
||||||
@ -196,7 +199,8 @@ class VSPRepository:
|
|||||||
:p_format, :p_opened_at, :p_placement_type, :p_staff_count,
|
:p_format, :p_opened_at, :p_placement_type, :p_staff_count,
|
||||||
:p_total_area, :p_closed_at, :p_is_active, :p_is_deleted,
|
:p_total_area, :p_closed_at, :p_is_active, :p_is_deleted,
|
||||||
:p_system_code, :p_updated_by, :p_vsp_type, :p_notes,
|
:p_system_code, :p_updated_by, :p_vsp_type, :p_notes,
|
||||||
:p_rent_contract_num, :p_rent_end_date
|
:p_location_form, :p_numbers, :p_rent_contract_num,
|
||||||
|
:p_rent_end_date
|
||||||
)
|
)
|
||||||
"""),
|
"""),
|
||||||
params,
|
params,
|
||||||
|
|||||||
@ -14,7 +14,7 @@ class AuthService:
|
|||||||
|
|
||||||
async def authenticate_user(self, username: str, password: str) -> Optional[Token]:
|
async def authenticate_user(self, username: str, password: str) -> Optional[Token]:
|
||||||
user = await self.user_repo.authenticate(username, password)
|
user = await self.user_repo.authenticate(username, password)
|
||||||
if not user or not user.is_active:
|
if not user:
|
||||||
return None
|
return None
|
||||||
access_token = create_access_token(data={"sub": user.username})
|
access_token = create_access_token(data={"sub": user.username})
|
||||||
refresh_token = create_refresh_token(data={"sub": user.username})
|
refresh_token = create_refresh_token(data={"sub": user.username})
|
||||||
@ -26,7 +26,7 @@ class AuthService:
|
|||||||
|
|
||||||
async def authenticate_user_via_email(self, email: str) -> Optional[Token]:
|
async def authenticate_user_via_email(self, email: str) -> Optional[Token]:
|
||||||
user = await self.user_repo.authenticate_via_email(email)
|
user = await self.user_repo.authenticate_via_email(email)
|
||||||
if not user or not user.is_active:
|
if not user:
|
||||||
return None
|
return None
|
||||||
access_token = create_access_token(data={"sub": user.email})
|
access_token = create_access_token(data={"sub": user.email})
|
||||||
refresh_token = create_refresh_token(data={"sub": user.email})
|
refresh_token = create_refresh_token(data={"sub": user.email})
|
||||||
|
|||||||
@ -80,25 +80,6 @@ class BudgetFormService:
|
|||||||
org_unit_id=org_unit_id,
|
org_unit_id=org_unit_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def create_if_not_exists(
|
|
||||||
self,
|
|
||||||
form_type_code: str,
|
|
||||||
year: int,
|
|
||||||
org_unit_id: int,
|
|
||||||
) -> BudgetForm | None:
|
|
||||||
existing_bf = await self.bf_repo.get(
|
|
||||||
form_type_code=form_type_code,
|
|
||||||
year=year,
|
|
||||||
org_unit=org_unit_id,
|
|
||||||
)
|
|
||||||
if existing_bf:
|
|
||||||
return existing_bf
|
|
||||||
return await self.bf_repo.create(
|
|
||||||
form_type_code=form_type_code,
|
|
||||||
year=year,
|
|
||||||
org_unit_id=org_unit_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def bulk_create(
|
async def bulk_create(
|
||||||
self,
|
self,
|
||||||
form_type_code: str,
|
form_type_code: str,
|
||||||
@ -115,20 +96,3 @@ class BudgetFormService:
|
|||||||
for org_unit_id in org_unit_ids
|
for org_unit_id in org_unit_ids
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
async def bulk_create_if_not_exists(
|
|
||||||
self,
|
|
||||||
form_type_code: str,
|
|
||||||
year: int,
|
|
||||||
org_unit_ids: list[int],
|
|
||||||
) -> BudgetForm | None:
|
|
||||||
return await asyncio.gather(
|
|
||||||
*[
|
|
||||||
self.create_if_not_exists(
|
|
||||||
form_type_code=form_type_code,
|
|
||||||
year=year,
|
|
||||||
org_unit_id=org_unit_id,
|
|
||||||
)
|
|
||||||
for org_unit_id in org_unit_ids
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.db.models.form_type import FormTypeEnum
|
|
||||||
from src.db.models.role import UserRoleEnum
|
from src.db.models.role import UserRoleEnum
|
||||||
from src.db.models.app_user import AppUser
|
from src.db.models.app_user import AppUser
|
||||||
from src.db.models.expense_item import ExpenseItem
|
from src.db.models.expense_item import ExpenseItem
|
||||||
@ -11,25 +10,7 @@ class ExpenseItemService:
|
|||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
self.repo = ExpenseItemRepository(db)
|
self.repo = ExpenseItemRepository(db)
|
||||||
|
|
||||||
async def get(
|
async def get(self, item_id: int, user: AppUser | None = None) -> ExpenseItem | None:
|
||||||
self,
|
|
||||||
item_id: int,
|
|
||||||
user: AppUser | None = None,
|
|
||||||
) -> ExpenseItem | None:
|
|
||||||
if not user or user.role_id == UserRoleEnum.ADMIN:
|
if not user or user.role_id == UserRoleEnum.ADMIN:
|
||||||
return await self.repo.get(item_id=item_id)
|
return await self.repo.get(item_id=item_id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def get_list(
|
|
||||||
self,
|
|
||||||
r_start: bool | None = None,
|
|
||||||
sheet: str | None = None,
|
|
||||||
direction: str | None = None,
|
|
||||||
form_type: FormTypeEnum | str | None = None,
|
|
||||||
) -> list[ExpenseItem]:
|
|
||||||
return await self.repo.get_list(
|
|
||||||
r_start=r_start,
|
|
||||||
sheet=sheet,
|
|
||||||
direction=direction,
|
|
||||||
form_type=form_type,
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1185,238 +1185,6 @@ FORM1_DEPTH_COLUMNS: dict = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
FORM2_DEPTH_AHR_SUB_COLUMNS: dict = {
|
|
||||||
"address": {
|
|
||||||
"key": None,
|
|
||||||
"name": "Адрес арендуемого помещения *",
|
|
||||||
"children": {
|
|
||||||
"address": {
|
|
||||||
"name": '(заполняется авто из табл. "INFO" в зависимости от выбранного объекта)'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"object": {
|
|
||||||
"key": None,
|
|
||||||
"name": "Объект **",
|
|
||||||
"children": {
|
|
||||||
"object_type": {
|
|
||||||
"name": '(необходимо выбрать из списка)'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"area": {
|
|
||||||
"key": None,
|
|
||||||
"name": "Арендуемая площадь",
|
|
||||||
"children": {
|
|
||||||
"rented_area": {
|
|
||||||
"name": 'в кв.м.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"date_end": {
|
|
||||||
"key": None,
|
|
||||||
"name": "Срок окончания договора",
|
|
||||||
"children": {
|
|
||||||
"contract_end_date": {
|
|
||||||
"name": 'в формате "месяц.год"'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"contract_number": {
|
|
||||||
"name": 'Номер договора'
|
|
||||||
},
|
|
||||||
"plan": {
|
|
||||||
# "key": None,
|
|
||||||
"name": "Планируемая сумма расходов на 2026 год",
|
|
||||||
"children": {
|
|
||||||
"q1": {
|
|
||||||
"name": "I квартал"
|
|
||||||
},
|
|
||||||
"q2": {
|
|
||||||
"name": "II квартал"
|
|
||||||
},
|
|
||||||
"q3": {
|
|
||||||
"name": "III квартал"
|
|
||||||
},
|
|
||||||
"q4": {
|
|
||||||
"name": "IV квартал"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"comment": {
|
|
||||||
"name": "Комментарий",
|
|
||||||
},
|
|
||||||
"fact_q1": {
|
|
||||||
# "key": None,
|
|
||||||
"name": "Фактические расходы за I кв 2026 года",
|
|
||||||
"children": {
|
|
||||||
"up_jan": {
|
|
||||||
"key": None,
|
|
||||||
"name": "январь",
|
|
||||||
"children": {
|
|
||||||
"jan": {
|
|
||||||
"name": "в тыс. рублей"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"up_feb": {
|
|
||||||
"key": None,
|
|
||||||
"name": "февраль",
|
|
||||||
"children": {
|
|
||||||
"feb": {
|
|
||||||
"name": "в тыс. рублей"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"up_mar": {
|
|
||||||
"key": None,
|
|
||||||
"name": "март",
|
|
||||||
"children": {
|
|
||||||
"mar": {
|
|
||||||
"name": "в тыс. рублей"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"up_total": {
|
|
||||||
"key": None,
|
|
||||||
"name": "Итог I кв",
|
|
||||||
"children": {
|
|
||||||
"total": {
|
|
||||||
"name": "в тыс. рублей"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fact_q2": {
|
|
||||||
# "key": None,
|
|
||||||
"name": "Фактические расходы за II кв",
|
|
||||||
"children": {
|
|
||||||
"up_apr": {
|
|
||||||
"key": None,
|
|
||||||
"name": "апрель",
|
|
||||||
"children": {
|
|
||||||
"apr": {
|
|
||||||
"name": "в тыс. рублей"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"up_may": {
|
|
||||||
"key": None,
|
|
||||||
"name": "май",
|
|
||||||
"children": {
|
|
||||||
"may": {
|
|
||||||
"name": "в тыс. рублей"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"up_jun": {
|
|
||||||
"key": None,
|
|
||||||
"name": "июнь",
|
|
||||||
"children": {
|
|
||||||
"jun": {
|
|
||||||
"name": "в тыс. рублей"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"up_total": {
|
|
||||||
"key": None,
|
|
||||||
"name": "Итог II кв",
|
|
||||||
"children": {
|
|
||||||
"total": {
|
|
||||||
"name": "в тыс. рублей"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fact_q3": {
|
|
||||||
# "key": None,
|
|
||||||
"name": "Фактические расходы за III кв",
|
|
||||||
"children": {
|
|
||||||
"up_jul": {
|
|
||||||
"key": None,
|
|
||||||
"name": "июль",
|
|
||||||
"children": {
|
|
||||||
"jul": {
|
|
||||||
"name": "в тыс. рублей"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"up_aug": {
|
|
||||||
"key": None,
|
|
||||||
"name": "август",
|
|
||||||
"children": {
|
|
||||||
"aug": {
|
|
||||||
"name": "в тыс. рублей"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"up_sep": {
|
|
||||||
"key": None,
|
|
||||||
"name": "сентябрь",
|
|
||||||
"children": {
|
|
||||||
"sep": {
|
|
||||||
"name": "в тыс. рублей"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"up_total": {
|
|
||||||
"key": None,
|
|
||||||
"name": "Итог III кв",
|
|
||||||
"children": {
|
|
||||||
"total": {
|
|
||||||
"name": "в тыс. рублей"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fact_q4": {
|
|
||||||
# "key": None,
|
|
||||||
"name": "Фактические расходы за IV кв",
|
|
||||||
"children": {
|
|
||||||
"up_oct": {
|
|
||||||
"key": None,
|
|
||||||
"name": "октябрь",
|
|
||||||
"children": {
|
|
||||||
"oct": {
|
|
||||||
"name": "в тыс. рублей"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"up_nov": {
|
|
||||||
"key": None,
|
|
||||||
"name": "ноябрь",
|
|
||||||
"children": {
|
|
||||||
"nov": {
|
|
||||||
"name": "в тыс. рублей"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"up_dec": {
|
|
||||||
"key": None,
|
|
||||||
"name": "декабрь",
|
|
||||||
"children": {
|
|
||||||
"dec": {
|
|
||||||
"name": "в тыс. рублей"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"up_total": {
|
|
||||||
"key": None,
|
|
||||||
"name": "Итог IV кв",
|
|
||||||
"children": {
|
|
||||||
"total": {
|
|
||||||
"name": "в тыс. рублей"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PALETTE = {
|
PALETTE = {
|
||||||
"green": {
|
"green": {
|
||||||
"ROOT": "C2D59A",
|
"ROOT": "C2D59A",
|
||||||
|
|||||||
@ -24,7 +24,6 @@ from src.services.export_service.export_normalizers import (
|
|||||||
from src.services.export_service.export_mappings import (
|
from src.services.export_service.export_mappings import (
|
||||||
FORM1_COLORS,
|
FORM1_COLORS,
|
||||||
FORM1_DEPTH_COLUMNS,
|
FORM1_DEPTH_COLUMNS,
|
||||||
FORM2_DEPTH_AHR_SUB_COLUMNS,
|
|
||||||
FORM3_REPORT_COLUMNS,
|
FORM3_REPORT_COLUMNS,
|
||||||
SMETA_COLUMNS,
|
SMETA_COLUMNS,
|
||||||
)
|
)
|
||||||
@ -40,17 +39,6 @@ ZIP_MEDIA_TYPE = "application/zip"
|
|||||||
FORM1_DIRECTION_REQUIRED_SHEETS = {"AHR", "CAP"}
|
FORM1_DIRECTION_REQUIRED_SHEETS = {"AHR", "CAP"}
|
||||||
SHEETS_WITH_SECTIONS = {"AHR", "CAP", "OPER"}
|
SHEETS_WITH_SECTIONS = {"AHR", "CAP", "OPER"}
|
||||||
|
|
||||||
SHEET_NAMES = {
|
|
||||||
'AHR': 'АХР',
|
|
||||||
'CAP': 'КВП',
|
|
||||||
'OPER': 'Операц',
|
|
||||||
'AHR_LIMIT': 'АХР Лимиты',
|
|
||||||
'AHR_RENT': 'АХР Аренда',
|
|
||||||
'AHR_SECURITY': 'АХР Безопасность',
|
|
||||||
'AHR_UTILITY': 'АХР Коммунальные услуги',
|
|
||||||
'SMETA': 'Смета',
|
|
||||||
}
|
|
||||||
SKIP_SHEETS = ('OTCH9F',)
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ExportSheetPlan:
|
class ExportSheetPlan:
|
||||||
@ -99,21 +87,6 @@ class ExportService:
|
|||||||
value_serializer=self.value_serializer.to_excel_value,
|
value_serializer=self.value_serializer.to_excel_value,
|
||||||
colors_config=FORM1_COLORS["CAP"],
|
colors_config=FORM1_COLORS["CAP"],
|
||||||
),
|
),
|
||||||
"AHR_RENT": DepthSheetWriter(
|
|
||||||
columns_dict=FORM2_DEPTH_AHR_SUB_COLUMNS,
|
|
||||||
row_normalizer=self.form1_row_normalizer.normalize,
|
|
||||||
value_serializer=self.value_serializer.to_excel_value,
|
|
||||||
),
|
|
||||||
"AHR_UTILITY": DepthSheetWriter(
|
|
||||||
columns_dict=FORM2_DEPTH_AHR_SUB_COLUMNS,
|
|
||||||
row_normalizer=self.form1_row_normalizer.normalize,
|
|
||||||
value_serializer=self.value_serializer.to_excel_value,
|
|
||||||
),
|
|
||||||
"AHR_SECURITY": DepthSheetWriter(
|
|
||||||
columns_dict=FORM2_DEPTH_AHR_SUB_COLUMNS,
|
|
||||||
row_normalizer=self.form1_row_normalizer.normalize,
|
|
||||||
value_serializer=self.value_serializer.to_excel_value,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
self.form3_report_writer = TabularSheetWriter(
|
self.form3_report_writer = TabularSheetWriter(
|
||||||
columns=FORM3_REPORT_COLUMNS,
|
columns=FORM3_REPORT_COLUMNS,
|
||||||
@ -244,9 +217,6 @@ class ExportService:
|
|||||||
workbook = self._create_workbook_with_metadata(form=form)
|
workbook = self._create_workbook_with_metadata(form=form)
|
||||||
exported_sheets = 0
|
exported_sheets = 0
|
||||||
for plan in plans:
|
for plan in plans:
|
||||||
if not plan.validation_direction and plan.sheet_name in SKIP_SHEETS:
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._validate_sheet_query_params(
|
self._validate_sheet_query_params(
|
||||||
form_type_code=form.form_type.code,
|
form_type_code=form.form_type.code,
|
||||||
sheet=plan.sheet_name,
|
sheet=plan.sheet_name,
|
||||||
@ -259,7 +229,7 @@ class ExportService:
|
|||||||
direction=plan.effective_direction,
|
direction=plan.effective_direction,
|
||||||
sections=plan.effective_sections,
|
sections=plan.effective_sections,
|
||||||
)
|
)
|
||||||
worksheet = workbook.create_sheet(title=self._safe_sheet_name(plan.sheet_name, plan.effective_direction))
|
worksheet = workbook.create_sheet(title=self._safe_sheet_name(plan.sheet_name))
|
||||||
self._write_sheet_rows(
|
self._write_sheet_rows(
|
||||||
worksheet=worksheet,
|
worksheet=worksheet,
|
||||||
rows=rows,
|
rows=rows,
|
||||||
@ -434,15 +404,7 @@ class ExportService:
|
|||||||
return form_type_code == FormTypeEnum.FORM_1 and sheet in FORM1_DIRECTION_REQUIRED_SHEETS
|
return form_type_code == FormTypeEnum.FORM_1 and sheet in FORM1_DIRECTION_REQUIRED_SHEETS
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _safe_sheet_name(value: str, direction: str | None = None) -> str:
|
def _safe_sheet_name(value: str) -> str:
|
||||||
if value in SHEET_NAMES:
|
|
||||||
sheet_name = SHEET_NAMES[value]
|
|
||||||
match direction:
|
|
||||||
case 'Support':
|
|
||||||
sheet_name = f'{sheet_name}_П'
|
|
||||||
case 'Development':
|
|
||||||
sheet_name = f'{sheet_name}_Р'
|
|
||||||
return sheet_name
|
|
||||||
safe = value
|
safe = value
|
||||||
for char in ("[", "]", ":", "*", "?", "/", "\\"):
|
for char in ("[", "]", ":", "*", "?", "/", "\\"):
|
||||||
safe = safe.replace(char, "_")
|
safe = safe.replace(char, "_")
|
||||||
|
|||||||
@ -1,106 +0,0 @@
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
from src.db.models.form3_phase import Form3Phase
|
|
||||||
from src.db.models.form_type import FormType, FormTypeEnum
|
|
||||||
from src.repository.form3_phase_repository import Form3PhaseRepository
|
|
||||||
from src.core.errors import AccessDeniedException, ValidationsError
|
|
||||||
from src.db.models.role import UserRoleEnum
|
|
||||||
from src.db.models.app_user import AppUser
|
|
||||||
from src.domain.schemas import Form3PhaseCreate, Form3PhaseUpdate
|
|
||||||
|
|
||||||
|
|
||||||
class Form3PhaseService:
|
|
||||||
def __init__(self, db: AsyncSession):
|
|
||||||
self.db = db
|
|
||||||
self.f3p_repo = Form3PhaseRepository(db)
|
|
||||||
|
|
||||||
async def get_list(
|
|
||||||
self,
|
|
||||||
report_id: int | list[int],
|
|
||||||
phase_code: str | None = None,
|
|
||||||
) -> list[Form3Phase]:
|
|
||||||
return await self.f3p_repo.get_list(
|
|
||||||
report_id=report_id,
|
|
||||||
phase_code=phase_code,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get(
|
|
||||||
self, report_id: int, phase_code: str, user: AppUser
|
|
||||||
) -> Form3Phase | None:
|
|
||||||
return await self.f3p_repo.get(
|
|
||||||
report_id=report_id,
|
|
||||||
phase_code=phase_code,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _validate_form3_phase(
|
|
||||||
self,
|
|
||||||
form_phase: Form3PhaseCreate | Form3PhaseUpdate,
|
|
||||||
) -> None:
|
|
||||||
errors = {}
|
|
||||||
if form_phase.column_keys:
|
|
||||||
form_type = FormType(code=FormTypeEnum.FORM_3)
|
|
||||||
for column_key in form_phase.column_keys:
|
|
||||||
section = column_key.split(".")
|
|
||||||
if section[0] not in form_type.section_list:
|
|
||||||
if "column_keys" in errors:
|
|
||||||
errors["column_keys"].append(f"Колонка {column_key} некорректна")
|
|
||||||
else:
|
|
||||||
errors["column_keys"] = [f"Колонка {column_key} некорректна"]
|
|
||||||
if errors:
|
|
||||||
raise ValidationsError(errors=errors)
|
|
||||||
|
|
||||||
|
|
||||||
async def create(
|
|
||||||
self,
|
|
||||||
report_id: int,
|
|
||||||
body: Form3PhaseCreate,
|
|
||||||
user: AppUser,
|
|
||||||
) -> Form3Phase:
|
|
||||||
if user.role_id != UserRoleEnum.ADMIN:
|
|
||||||
raise AccessDeniedException
|
|
||||||
|
|
||||||
self._validate_form3_phase(
|
|
||||||
form_phase=body,
|
|
||||||
)
|
|
||||||
return await self.f3p_repo.create(
|
|
||||||
report_id=report_id,
|
|
||||||
phase_code=body.phase_code,
|
|
||||||
role=body.role,
|
|
||||||
column_keys=body.column_keys,
|
|
||||||
opens_at=body.opens_at,
|
|
||||||
closes_at=body.closes_at,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def update(
|
|
||||||
self,
|
|
||||||
report_id: int,
|
|
||||||
phase_code: str,
|
|
||||||
body: Form3PhaseUpdate,
|
|
||||||
user: AppUser,
|
|
||||||
) -> Form3Phase:
|
|
||||||
if user.role_id != UserRoleEnum.ADMIN:
|
|
||||||
raise AccessDeniedException
|
|
||||||
|
|
||||||
self._validate_form3_phase(
|
|
||||||
form_phase=body,
|
|
||||||
)
|
|
||||||
|
|
||||||
data = body.model_dump(exclude_unset=True)
|
|
||||||
if not data:
|
|
||||||
return await self.f3p_repo.get(
|
|
||||||
report_id=report_id,
|
|
||||||
phase_code=phase_code,
|
|
||||||
)
|
|
||||||
return await self.f3p_repo.update(
|
|
||||||
report_id=report_id,
|
|
||||||
phase_code=phase_code,
|
|
||||||
data=data,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def delete(
|
|
||||||
self, report_id: int, phase_code: str, user: AppUser,
|
|
||||||
) -> bool:
|
|
||||||
return await self.f3p_repo.delete(
|
|
||||||
report_id=report_id,
|
|
||||||
phase_code=phase_code,
|
|
||||||
)
|
|
||||||
@ -54,7 +54,6 @@ class OrgUnitService:
|
|||||||
self,
|
self,
|
||||||
title: str,
|
title: str,
|
||||||
is_ssp: bool,
|
is_ssp: bool,
|
||||||
utc_offset: str,
|
|
||||||
user: AppUser,
|
user: AppUser,
|
||||||
) -> OrgUnit:
|
) -> OrgUnit:
|
||||||
"""Создание нового ССП/РФ."""
|
"""Создание нового ССП/РФ."""
|
||||||
@ -64,7 +63,6 @@ class OrgUnitService:
|
|||||||
return await self.org_repo.create(
|
return await self.org_repo.create(
|
||||||
title=title,
|
title=title,
|
||||||
is_ssp=is_ssp,
|
is_ssp=is_ssp,
|
||||||
utc_offset=utc_offset,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def update(
|
async def update(
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.db.models.project import Project
|
|
||||||
from src.db.models.app_user import AppUser
|
from src.db.models.app_user import AppUser
|
||||||
from src.db.models.role import UserRoleEnum
|
from src.db.models.role import UserRoleEnum
|
||||||
from src.core.errors import AccessDeniedException, ValidationException
|
from src.core.errors import AccessDeniedException, ValidationException
|
||||||
@ -42,10 +41,6 @@ class ProjectService:
|
|||||||
project["reports"] = await self.project_repo.get_reports(project_id=project_id)
|
project["reports"] = await self.project_repo.get_reports(project_id=project_id)
|
||||||
return project
|
return project
|
||||||
|
|
||||||
async def get_instance(self, project_id: int, user: AppUser) -> Project | None:
|
|
||||||
org_unit_ids = await self._allowed_org_unit_ids(user)
|
|
||||||
return await self.project_repo.get_instance(project_id=project_id, org_unit_ids=org_unit_ids)
|
|
||||||
|
|
||||||
async def resolve_report_id(
|
async def resolve_report_id(
|
||||||
self,
|
self,
|
||||||
project_id: int,
|
project_id: int,
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
|
|
||||||
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)
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
|
|
||||||
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.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,
|
|
||||||
load_project: bool = False,
|
|
||||||
) -> RfProjectReport | None:
|
|
||||||
return await self.pr_repo.get(
|
|
||||||
report_id=report_id,
|
|
||||||
year=year,
|
|
||||||
report_type=report_type,
|
|
||||||
project_id=project_id,
|
|
||||||
load_project=load_project,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_list(
|
|
||||||
self,
|
|
||||||
report_id: int | None = None,
|
|
||||||
year: int | None = None,
|
|
||||||
report_type: str | None = None,
|
|
||||||
project_id: int | None = None,
|
|
||||||
) -> list[RfProjectReport]:
|
|
||||||
return await self.pr_repo.get_list(
|
|
||||||
report_id=report_id,
|
|
||||||
year=year,
|
|
||||||
report_type=report_type,
|
|
||||||
project_id=project_id,
|
|
||||||
)
|
|
||||||
@ -1,14 +1,9 @@
|
|||||||
|
|
||||||
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, SheetValidationError
|
from src.repository.sheet_repository import SheetRepository
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -16,9 +11,6 @@ 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,
|
||||||
@ -35,6 +27,28 @@ class SheetService:
|
|||||||
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,
|
||||||
form_id: int,
|
form_id: int,
|
||||||
@ -110,66 +124,6 @@ class SheetService:
|
|||||||
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,
|
||||||
form_id: int,
|
form_id: int,
|
||||||
|
|||||||
@ -11,7 +11,6 @@ from src.repository.budget_form_repository import BudgetFormRepository
|
|||||||
from src.core.errors import AccessDeniedException, ValidationException
|
from src.core.errors import AccessDeniedException, ValidationException
|
||||||
from src.db.models import AppUser, UserRoleEnum, Vsp
|
from src.db.models import AppUser, UserRoleEnum, Vsp
|
||||||
from src.domain.schemas import VSPCreate, VSPUpdate
|
from src.domain.schemas import VSPCreate, VSPUpdate
|
||||||
from src.repository.org_unit_repository import OrgUnitRepository
|
|
||||||
from src.repository.user_repository import UserRepository
|
from src.repository.user_repository import UserRepository
|
||||||
from src.repository.vsp_repository import VSPRepository
|
from src.repository.vsp_repository import VSPRepository
|
||||||
|
|
||||||
@ -21,8 +20,8 @@ _info_close_sync_lock = asyncio.Lock()
|
|||||||
|
|
||||||
class VSPService:
|
class VSPService:
|
||||||
EXECUTOR_EDITABLE_FIELDS = {
|
EXECUTOR_EDITABLE_FIELDS = {
|
||||||
"placement_type",
|
"location_form",
|
||||||
"staff_count",
|
"numbers",
|
||||||
"total_area",
|
"total_area",
|
||||||
"rent_contract_num",
|
"rent_contract_num",
|
||||||
"rent_end_date",
|
"rent_end_date",
|
||||||
@ -32,7 +31,6 @@ class VSPService:
|
|||||||
self.db = db
|
self.db = db
|
||||||
self.vsp_repo = VSPRepository(db)
|
self.vsp_repo = VSPRepository(db)
|
||||||
self.form_repo = BudgetFormRepository(db)
|
self.form_repo = BudgetFormRepository(db)
|
||||||
self.org_unit_repo = OrgUnitRepository(db)
|
|
||||||
self.user_repo = UserRepository(db)
|
self.user_repo = UserRepository(db)
|
||||||
|
|
||||||
async def get(self, vsp_id: int, user: AppUser, load_org_unit: bool = False) -> Optional[Vsp]:
|
async def get(self, vsp_id: int, user: AppUser, load_org_unit: bool = False) -> Optional[Vsp]:
|
||||||
@ -56,9 +54,9 @@ class VSPService:
|
|||||||
ssp_ids: list[int] | None = None,
|
ssp_ids: list[int] | None = None,
|
||||||
open_date_start: date | None = None,
|
open_date_start: date | None = None,
|
||||||
open_date_end: date | None = None,
|
open_date_end: date | None = None,
|
||||||
placement_type: str | None = None,
|
location_form: str | None = None,
|
||||||
staff_count_min: int | None = None,
|
numbers_min: int | None = None,
|
||||||
staff_count_max: int | None = None,
|
numbers_max: int | None = None,
|
||||||
is_active: bool | None = None,
|
is_active: bool | None = None,
|
||||||
load_org_unit: bool = False,
|
load_org_unit: bool = False,
|
||||||
with_count: bool = False,
|
with_count: bool = False,
|
||||||
@ -86,9 +84,9 @@ class VSPService:
|
|||||||
ssp_ids=effective_ssp_ids,
|
ssp_ids=effective_ssp_ids,
|
||||||
open_date_start=open_date_start,
|
open_date_start=open_date_start,
|
||||||
open_date_end=open_date_end,
|
open_date_end=open_date_end,
|
||||||
placement_type=placement_type,
|
location_form=location_form,
|
||||||
staff_count_min=staff_count_min,
|
numbers_min=numbers_min,
|
||||||
staff_count_max=staff_count_max,
|
numbers_max=numbers_max,
|
||||||
is_active=is_active,
|
is_active=is_active,
|
||||||
load_org_unit=load_org_unit,
|
load_org_unit=load_org_unit,
|
||||||
with_count=with_count,
|
with_count=with_count,
|
||||||
@ -120,17 +118,16 @@ class VSPService:
|
|||||||
else:
|
else:
|
||||||
ssp_ids = list(checking_ssps & set(ssp_ids))
|
ssp_ids = list(checking_ssps & set(ssp_ids))
|
||||||
|
|
||||||
await self._sync_closed_records()
|
|
||||||
return await self.vsp_repo.get_list(
|
return await self.vsp_repo.get_list(
|
||||||
ssp_ids=ssp_ids,
|
ssp_ids=ssp_ids,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
|
close_date_is_null=True,
|
||||||
with_count=with_count,
|
with_count=with_count,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def create(self, payload: VSPCreate, user: AppUser, load_org_unit: bool = False) -> Vsp:
|
async def create(self, payload: VSPCreate, user: AppUser, load_org_unit: bool = False) -> Vsp:
|
||||||
if not self._can_modify(user):
|
if not self._can_modify(user):
|
||||||
raise AccessDeniedException()
|
raise AccessDeniedException()
|
||||||
await self._ensure_active_branch(payload.branch_id)
|
|
||||||
try:
|
try:
|
||||||
created = await self.vsp_repo.create(payload=payload, created_by=user.id, load_org_unit=load_org_unit)
|
created = await self.vsp_repo.create(payload=payload, created_by=user.id, load_org_unit=load_org_unit)
|
||||||
if created.closed_at is not None and created.closed_at <= date.today():
|
if created.closed_at is not None and created.closed_at <= date.today():
|
||||||
@ -147,8 +144,6 @@ class VSPService:
|
|||||||
raise ValidationException(description="Указанный номер уже существует")
|
raise ValidationException(description="Указанный номер уже существует")
|
||||||
if "chk_closed_after_opened" in message:
|
if "chk_closed_after_opened" in message:
|
||||||
raise ValidationException(description="Дата закрытия не может быть меньше даты открытия")
|
raise ValidationException(description="Дата закрытия не может быть меньше даты открытия")
|
||||||
if "chk_rent_date" in message:
|
|
||||||
raise ValidationException(description="Срок окончания договора аренды не может быть меньше даты открытия")
|
|
||||||
raise ValidationException(description="Некорректные данные для записи ВСП") from exc
|
raise ValidationException(description="Некорректные данные для записи ВСП") from exc
|
||||||
|
|
||||||
async def export_xlsx(
|
async def export_xlsx(
|
||||||
@ -159,9 +154,9 @@ class VSPService:
|
|||||||
ssp_ids: list[int] | None = None,
|
ssp_ids: list[int] | None = None,
|
||||||
open_date_start: date | None = None,
|
open_date_start: date | None = None,
|
||||||
open_date_end: date | None = None,
|
open_date_end: date | None = None,
|
||||||
placement_type: str | None = None,
|
location_form: str | None = None,
|
||||||
staff_count_min: int | None = None,
|
numbers_min: int | None = None,
|
||||||
staff_count_max: int | None = None,
|
numbers_max: int | None = None,
|
||||||
) -> tuple[io.BytesIO, str]:
|
) -> tuple[io.BytesIO, str]:
|
||||||
data = await self.get_list(
|
data = await self.get_list(
|
||||||
user=user,
|
user=user,
|
||||||
@ -170,9 +165,9 @@ class VSPService:
|
|||||||
ssp_ids=ssp_ids,
|
ssp_ids=ssp_ids,
|
||||||
open_date_start=open_date_start,
|
open_date_start=open_date_start,
|
||||||
open_date_end=open_date_end,
|
open_date_end=open_date_end,
|
||||||
placement_type=placement_type,
|
location_form=location_form,
|
||||||
staff_count_min=staff_count_min,
|
numbers_min=numbers_min,
|
||||||
staff_count_max=staff_count_max,
|
numbers_max=numbers_max,
|
||||||
is_active=None,
|
is_active=None,
|
||||||
load_org_unit=True,
|
load_org_unit=True,
|
||||||
)
|
)
|
||||||
@ -193,8 +188,8 @@ class VSPService:
|
|||||||
"open_date",
|
"open_date",
|
||||||
"close_date",
|
"close_date",
|
||||||
"notes",
|
"notes",
|
||||||
"placement_type",
|
"location_form",
|
||||||
"staff_count",
|
"numbers",
|
||||||
"area",
|
"area",
|
||||||
"rent_contract_num",
|
"rent_contract_num",
|
||||||
"rent_end_date",
|
"rent_end_date",
|
||||||
@ -220,8 +215,8 @@ class VSPService:
|
|||||||
item.opened_at.isoformat() if item.opened_at else None,
|
item.opened_at.isoformat() if item.opened_at else None,
|
||||||
item.closed_at.isoformat() if item.closed_at else None,
|
item.closed_at.isoformat() if item.closed_at else None,
|
||||||
item.notes or None,
|
item.notes or None,
|
||||||
item.placement_type or None,
|
item.location_form or None,
|
||||||
item.staff_count if item.staff_count is not None else None,
|
item.numbers if item.numbers is not None else None,
|
||||||
float(item.total_area) if item.total_area is not None else None,
|
float(item.total_area) if item.total_area is not None else None,
|
||||||
item.rent_contract_num or None,
|
item.rent_contract_num or None,
|
||||||
item.rent_end_date.isoformat() if item.rent_end_date else None,
|
item.rent_end_date.isoformat() if item.rent_end_date else None,
|
||||||
@ -250,9 +245,6 @@ class VSPService:
|
|||||||
if not data:
|
if not data:
|
||||||
return vsp
|
return vsp
|
||||||
|
|
||||||
if "branch_id" in data:
|
|
||||||
await self._ensure_active_branch(data["branch_id"])
|
|
||||||
|
|
||||||
if user.role_id != UserRoleEnum.ADMIN:
|
if user.role_id != UserRoleEnum.ADMIN:
|
||||||
ssp_ids = await self._get_scoped_ssp_ids(user=user)
|
ssp_ids = await self._get_scoped_ssp_ids(user=user)
|
||||||
if not ssp_ids or vsp.branch_id not in set(ssp_ids):
|
if not ssp_ids or vsp.branch_id not in set(ssp_ids):
|
||||||
@ -274,8 +266,6 @@ class VSPService:
|
|||||||
raise ValidationException(description="Указанный номер уже существует")
|
raise ValidationException(description="Указанный номер уже существует")
|
||||||
if "chk_closed_after_opened" in message:
|
if "chk_closed_after_opened" in message:
|
||||||
raise ValidationException(description="Дата закрытия не может быть меньше даты открытия")
|
raise ValidationException(description="Дата закрытия не может быть меньше даты открытия")
|
||||||
if "chk_rent_date" in message:
|
|
||||||
raise ValidationException(description="Срок окончания договора аренды не может быть меньше даты открытия")
|
|
||||||
raise ValidationException(description="Некорректные данные для записи ВСП") from exc
|
raise ValidationException(description="Некорректные данные для записи ВСП") from exc
|
||||||
|
|
||||||
async def logical_delete(self, vsp_id: int, user: AppUser) -> bool:
|
async def logical_delete(self, vsp_id: int, user: AppUser) -> bool:
|
||||||
@ -301,10 +291,3 @@ class VSPService:
|
|||||||
return
|
return
|
||||||
await self.vsp_repo.deactivate_by_close_date(as_of=today)
|
await self.vsp_repo.deactivate_by_close_date(as_of=today)
|
||||||
_last_info_close_sync_run = today
|
_last_info_close_sync_run = today
|
||||||
|
|
||||||
async def _ensure_active_branch(self, branch_id: int | None) -> None:
|
|
||||||
if branch_id is None:
|
|
||||||
return
|
|
||||||
org_unit = await self.org_unit_repo.get(org_unit_id=branch_id)
|
|
||||||
if org_unit is None or not org_unit.is_active:
|
|
||||||
raise ValidationException(description="Указан неактивный или отсутствующий ССП/РФ")
|
|
||||||
|
|||||||
@ -30,28 +30,3 @@ def test_expense_item_get_executor(client, isp_tokens, auth_headers):
|
|||||||
headers=auth_headers(isp_tokens),
|
headers=auth_headers(isp_tokens),
|
||||||
)
|
)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
def test_expense_item_list(client, isp_tokens, auth_headers):
|
|
||||||
response = client.get(
|
|
||||||
"/api/v1/expense-item/",
|
|
||||||
headers=auth_headers(isp_tokens),
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
result = response.json()
|
|
||||||
assert result["success"] is True
|
|
||||||
assert isinstance(result["result"], list)
|
|
||||||
assert len(result["result"]) != 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_expense_item_list_r_start(client, isp_tokens, auth_headers):
|
|
||||||
response = client.get(
|
|
||||||
"/api/v1/expense-item/?r_start=true",
|
|
||||||
headers=auth_headers(isp_tokens),
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
result = response.json()
|
|
||||||
assert result["success"] is True
|
|
||||||
assert isinstance(result["result"], list)
|
|
||||||
assert len(result["result"]) != 0
|
|
||||||
assert all([el["item_id"].startswith("R") for el in result["result"]])
|
|
||||||
@ -84,16 +84,16 @@ def test_form_phases_delete_forbidden(client, isp_tokens):
|
|||||||
|
|
||||||
def test_form_phases_create(client, admin_tokens):
|
def test_form_phases_create(client, admin_tokens):
|
||||||
body = {
|
body = {
|
||||||
"budget_form_id": 4,
|
"budget_form_id": 1,
|
||||||
"sheet": "AHR",
|
"sheet": "AHR",
|
||||||
"phase_code": "new_phase",
|
"phase_code": "new_phase",
|
||||||
"role": "DFIP",
|
"role": "DFIP",
|
||||||
"column_keys": ["plan.q1"],
|
"column_keys": ["plan.q1"],
|
||||||
"opens_at": "2026-01-01T00:00:00",
|
"opens_at": "2026-01-01T00:00:00Z",
|
||||||
"closes_at": "2026-12-31T00:00:00",
|
"closes_at": "2026-12-31T00:00:00Z",
|
||||||
}
|
}
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/stages/form/4",
|
"/api/v1/stages/form/1",
|
||||||
json=body,
|
json=body,
|
||||||
headers=_auth_headers(admin_tokens),
|
headers=_auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
@ -102,31 +102,22 @@ def test_form_phases_create(client, admin_tokens):
|
|||||||
assert "success" in payload
|
assert "success" in payload
|
||||||
assert payload["success"]
|
assert payload["success"]
|
||||||
assert "result" in payload
|
assert "result" in payload
|
||||||
result = payload["result"]
|
assert isinstance(payload["result"], dict)
|
||||||
assert isinstance(result, dict)
|
|
||||||
opens_at = result.pop("opens_at", None)
|
|
||||||
assert "+05" in opens_at
|
|
||||||
assert body.pop("opens_at") == opens_at.split("+")[0]
|
|
||||||
closes_at = result.pop("closes_at", None)
|
|
||||||
assert "+05" in closes_at
|
|
||||||
assert body.pop("closes_at") == closes_at.split("+")[0]
|
|
||||||
assert result == body
|
|
||||||
|
|
||||||
assert payload["result"] == body
|
assert payload["result"] == body
|
||||||
|
|
||||||
|
|
||||||
def test_form_phases_update(client, admin_tokens):
|
def test_form_phases_update(client, admin_tokens):
|
||||||
body = {
|
body = {
|
||||||
"budget_form_id": 4,
|
"budget_form_id": 1,
|
||||||
"sheet": "AHR",
|
"sheet": "AHR",
|
||||||
"phase_code": "test",
|
"phase_code": "test",
|
||||||
"role": "EXECUTOR_RF",
|
"role": "EXECUTOR_RF",
|
||||||
"column_keys": ["plan.q1"],
|
"column_keys": ["plan.q1"],
|
||||||
"opens_at": "2026-01-01T00:00:00",
|
"opens_at": "2026-01-01T00:00:00Z",
|
||||||
"closes_at": "2026-12-31T00:00:00",
|
"closes_at": "2026-12-31T00:00:00Z",
|
||||||
}
|
}
|
||||||
response = client.patch(
|
response = client.patch(
|
||||||
"/api/v1/stages/form/4/AHR/test",
|
"/api/v1/stages/form/1/AHR/test",
|
||||||
json=body,
|
json=body,
|
||||||
headers=_auth_headers(admin_tokens),
|
headers=_auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
@ -136,15 +127,8 @@ def test_form_phases_update(client, admin_tokens):
|
|||||||
assert "success" in payload
|
assert "success" in payload
|
||||||
assert payload["success"]
|
assert payload["success"]
|
||||||
assert "result" in payload
|
assert "result" in payload
|
||||||
result = payload["result"]
|
assert isinstance(payload["result"], dict)
|
||||||
assert isinstance(result, dict)
|
assert payload["result"] == body
|
||||||
opens_at = result.pop("opens_at", None)
|
|
||||||
assert "+05" in opens_at
|
|
||||||
assert body.pop("opens_at") == opens_at.split("+")[0]
|
|
||||||
closes_at = result.pop("closes_at", None)
|
|
||||||
assert "+05" in closes_at
|
|
||||||
assert body.pop("closes_at") == closes_at.split("+")[0]
|
|
||||||
assert result == body
|
|
||||||
|
|
||||||
|
|
||||||
def test_form_phases_delete(client, admin_tokens):
|
def test_form_phases_delete(client, admin_tokens):
|
||||||
@ -154,165 +138,3 @@ def test_form_phases_delete(client, admin_tokens):
|
|||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
def test_form3_phases_list_smoke(client, admin_tokens):
|
|
||||||
response = client.get(
|
|
||||||
"/api/v1/stages/project/2", headers=_auth_headers(admin_tokens)
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
assert "result" in payload
|
|
||||||
assert "count" in payload
|
|
||||||
assert isinstance(payload["result"], list)
|
|
||||||
|
|
||||||
|
|
||||||
def test_form3_phases_list_filter_smoke(client, admin_tokens):
|
|
||||||
|
|
||||||
response = client.get(
|
|
||||||
"/api/v1/stages/project/2", headers=_auth_headers(admin_tokens)
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
assert "count" in payload
|
|
||||||
count = payload["count"]
|
|
||||||
|
|
||||||
response = client.get(
|
|
||||||
"/api/v1/stages/project/2?&phase_code=test", headers=_auth_headers(admin_tokens)
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
assert "count" in payload
|
|
||||||
assert payload["count"] <= count
|
|
||||||
|
|
||||||
|
|
||||||
def test_form3_phases_create_forbidden(client, isp_tokens):
|
|
||||||
response = client.post(
|
|
||||||
"/api/v1/stages/project/2",
|
|
||||||
json={
|
|
||||||
"year": 2026,
|
|
||||||
"report_type": "LIMIT",
|
|
||||||
"phase_code": "CODE1",
|
|
||||||
"role": "DFIP",
|
|
||||||
"column_keys": [
|
|
||||||
"q1.adj_by_items",
|
|
||||||
"q1.adj_increase"
|
|
||||||
],
|
|
||||||
"opens_at": "2027-07-20T00:00:00",
|
|
||||||
"closes_at": "2027-09-21T00:00:00"
|
|
||||||
},
|
|
||||||
headers=_auth_headers(isp_tokens),
|
|
||||||
)
|
|
||||||
assert response.status_code == 403
|
|
||||||
payload = response.json()
|
|
||||||
assert isinstance(payload, dict)
|
|
||||||
assert "message" in payload
|
|
||||||
|
|
||||||
|
|
||||||
def test_form3_phases_update_forbidden(client, isp_tokens):
|
|
||||||
response = client.patch(
|
|
||||||
"/api/v1/stages/project/2/2026/LIMIT/RF_FILL_2026",
|
|
||||||
json={
|
|
||||||
"role": "DFIP",
|
|
||||||
"column_keys": [
|
|
||||||
"q1.adj_by_items",
|
|
||||||
"q1.adj_increase"
|
|
||||||
],
|
|
||||||
"opens_at": "2027-07-20T00:00:00",
|
|
||||||
"closes_at": "2027-09-21T00:00:00"
|
|
||||||
},
|
|
||||||
headers=_auth_headers(isp_tokens),
|
|
||||||
)
|
|
||||||
assert response.status_code == 403
|
|
||||||
payload = response.json()
|
|
||||||
assert isinstance(payload, dict)
|
|
||||||
assert "message" in payload
|
|
||||||
|
|
||||||
|
|
||||||
def test_form3_phases_delete_forbidden(client, isp_tokens):
|
|
||||||
response = client.delete(
|
|
||||||
"/api/v1/stages/project/2/2026/LIMIT/RF_FILL_2026",
|
|
||||||
headers=_auth_headers(isp_tokens),
|
|
||||||
)
|
|
||||||
assert response.status_code == 403
|
|
||||||
payload = response.json()
|
|
||||||
assert isinstance(payload, dict)
|
|
||||||
assert "message" in payload
|
|
||||||
|
|
||||||
|
|
||||||
def test_form3_phases_create(client, admin_tokens):
|
|
||||||
body = {
|
|
||||||
"year": 2026,
|
|
||||||
"report_type": "LIMIT",
|
|
||||||
"phase_code": "CODE1",
|
|
||||||
"role": "DFIP",
|
|
||||||
"column_keys": [
|
|
||||||
"q1.adj_by_items",
|
|
||||||
"q1.adj_increase"
|
|
||||||
],
|
|
||||||
"opens_at": "2027-07-20T00:00:00",
|
|
||||||
"closes_at": "2027-09-21T00:00:00"
|
|
||||||
}
|
|
||||||
response = client.post(
|
|
||||||
"/api/v1/stages/project/2",
|
|
||||||
json=body,
|
|
||||||
headers=_auth_headers(admin_tokens),
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
assert "success" in payload
|
|
||||||
assert payload["success"]
|
|
||||||
assert "result" in payload
|
|
||||||
result = payload["result"]
|
|
||||||
assert isinstance(result, dict)
|
|
||||||
opens_at = result.pop("opens_at", None)
|
|
||||||
assert "+03" in opens_at
|
|
||||||
assert body.pop("opens_at") == opens_at.split("+")[0]
|
|
||||||
closes_at = result.pop("closes_at", None)
|
|
||||||
assert "+03" in closes_at
|
|
||||||
assert body.pop("closes_at") == closes_at.split("+")[0]
|
|
||||||
assert result == body
|
|
||||||
|
|
||||||
assert payload["result"] == body
|
|
||||||
|
|
||||||
|
|
||||||
def test_form3_phases_update(client, admin_tokens):
|
|
||||||
body = {
|
|
||||||
"role": "DFIP",
|
|
||||||
"column_keys": [
|
|
||||||
"q1.adj_by_items",
|
|
||||||
"q1.adj_increase"
|
|
||||||
],
|
|
||||||
"opens_at": "2027-07-20T00:00:00",
|
|
||||||
"closes_at": "2027-09-21T00:00:00"
|
|
||||||
}
|
|
||||||
response = client.patch(
|
|
||||||
"/api/v1/stages/project/2/2026/LIMIT/RF_FILL_2026",
|
|
||||||
json=body,
|
|
||||||
headers=_auth_headers(admin_tokens),
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
assert "success" in payload
|
|
||||||
assert payload["success"]
|
|
||||||
assert "result" in payload
|
|
||||||
result = payload["result"]
|
|
||||||
assert isinstance(result, dict)
|
|
||||||
opens_at = result.pop("opens_at", None)
|
|
||||||
assert "+03" in opens_at
|
|
||||||
assert body.pop("opens_at") == opens_at.split("+")[0]
|
|
||||||
closes_at = result.pop("closes_at", None)
|
|
||||||
assert "+03" in closes_at
|
|
||||||
assert body.pop("closes_at") == closes_at.split("+")[0]
|
|
||||||
|
|
||||||
body.update({'phase_code': 'RF_FILL_2026', 'report_type': 'LIMIT', 'year': 2026})
|
|
||||||
assert result == body
|
|
||||||
|
|
||||||
|
|
||||||
def test_form3_phases_delete(client, admin_tokens):
|
|
||||||
response = client.delete(
|
|
||||||
"/api/v1/stages/project/2/2026/LIMIT/RF_FILL_2026",
|
|
||||||
headers=_auth_headers(admin_tokens),
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
|
|
||||||
|
|||||||
@ -161,25 +161,6 @@ def test_forms_add_form_success(client, admin_tokens, auth_headers):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
def test_forms_add_form_existing_unit(client, admin_tokens, auth_headers):
|
|
||||||
response = client.post(
|
|
||||||
"/api/v1/form/",
|
|
||||||
json={"year": 2026, "form_type_code": "FORM_2", "org_unit_ids": [1, 2]},
|
|
||||||
headers=auth_headers(admin_tokens),
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
assert "result" in payload
|
|
||||||
assert isinstance(payload["result"], list)
|
|
||||||
assert len(payload["result"]) == 2
|
|
||||||
assert "year" in payload["result"][0]
|
|
||||||
assert payload["result"][0]["year"] == 2026
|
|
||||||
|
|
||||||
assert "id" in payload["result"][1]
|
|
||||||
assert payload["result"][1]["id"] == 4
|
|
||||||
|
|
||||||
|
|
||||||
def test_forms_add_form_org_unit_not_found(client, admin_tokens, auth_headers):
|
def test_forms_add_form_org_unit_not_found(client, admin_tokens, auth_headers):
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/form/",
|
"/api/v1/form/",
|
||||||
|
|||||||
@ -78,89 +78,6 @@ def test_org_units_get_not_found(client, admin_tokens, auth_headers):
|
|||||||
# assert payload["result"]["id"] == 1
|
# assert payload["result"]["id"] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_org_units_update_offset_smoke(client, admin_tokens, auth_headers):
|
|
||||||
old_offset = "+05"
|
|
||||||
new_offset = "+04"
|
|
||||||
response = client.get(
|
|
||||||
"/api/v1/stages/form/4?sheet=AHR&phase_code=test", headers=auth_headers(admin_tokens)
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
assert "result" in payload
|
|
||||||
phases = payload["result"]
|
|
||||||
assert isinstance(phases, list)
|
|
||||||
assert len(phases)
|
|
||||||
phase_old = phases[0]
|
|
||||||
|
|
||||||
response = client.patch(
|
|
||||||
"/api/v1/org-unit/2",
|
|
||||||
json={"utc_offset": new_offset},
|
|
||||||
headers=auth_headers(admin_tokens),
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
assert payload["result"]["id"] == 2
|
|
||||||
assert payload["result"]["utc_offset"] == new_offset
|
|
||||||
|
|
||||||
response = client.get(
|
|
||||||
"/api/v1/stages/form/4?sheet=AHR&phase_code=test", headers=auth_headers(admin_tokens)
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
assert "result" in payload
|
|
||||||
phases = payload["result"]
|
|
||||||
assert isinstance(phases, list)
|
|
||||||
assert len(phases)
|
|
||||||
phase_new = phases[0]
|
|
||||||
|
|
||||||
assert new_offset in phase_new["opens_at"]
|
|
||||||
assert new_offset in phase_new["closes_at"]
|
|
||||||
assert phase_old["opens_at"].split(old_offset)[0] == phase_new["opens_at"].split(new_offset)[0]
|
|
||||||
assert phase_old["closes_at"].split(old_offset)[0] == phase_new["closes_at"].split(new_offset)[0]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_org_units_form3_update_offset_smoke(client, admin_tokens, auth_headers):
|
|
||||||
old_offset = "+03"
|
|
||||||
new_offset = "+04"
|
|
||||||
response = client.get(
|
|
||||||
"/api/v1/stages/project/2", headers=auth_headers(admin_tokens)
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
assert "result" in payload
|
|
||||||
phases = payload["result"]
|
|
||||||
assert isinstance(phases, list)
|
|
||||||
assert len(phases)
|
|
||||||
phase_old = phases[0]
|
|
||||||
|
|
||||||
response = client.patch(
|
|
||||||
"/api/v1/org-unit/1",
|
|
||||||
json={"utc_offset": new_offset},
|
|
||||||
headers=auth_headers(admin_tokens),
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
assert payload["result"]["id"] == 1
|
|
||||||
assert payload["result"]["utc_offset"] == new_offset
|
|
||||||
|
|
||||||
response = client.get(
|
|
||||||
"/api/v1/stages/project/2", headers=auth_headers(admin_tokens)
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
assert "result" in payload
|
|
||||||
phases = payload["result"]
|
|
||||||
assert isinstance(phases, list)
|
|
||||||
assert len(phases)
|
|
||||||
phase_new = phases[0]
|
|
||||||
|
|
||||||
assert new_offset in phase_new["opens_at"]
|
|
||||||
assert new_offset in phase_new["closes_at"]
|
|
||||||
assert phase_old["opens_at"].split(old_offset)[0] == phase_new["opens_at"].split(new_offset)[0]
|
|
||||||
assert phase_old["closes_at"].split(old_offset)[0] == phase_new["closes_at"].split(new_offset)[0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_org_units_update_not_found_error_shape(client, admin_tokens, auth_headers):
|
def test_org_units_update_not_found_error_shape(client, admin_tokens, auth_headers):
|
||||||
response = client.patch(
|
response = client.patch(
|
||||||
"/api/v1/org-unit/99999",
|
"/api/v1/org-unit/99999",
|
||||||
|
|||||||
@ -48,31 +48,6 @@ def _add_line(client, admin_tokens, auth_headers, project_id: int) -> int:
|
|||||||
pytest.skip("Не удалось подобрать expense_item_id для add_form3_line в текущей БД")
|
pytest.skip("Не удалось подобрать expense_item_id для add_form3_line в текущей БД")
|
||||||
|
|
||||||
|
|
||||||
def test_create_project_smoke(client, admin_tokens, auth_headers):
|
|
||||||
name = f"PT_{uuid.uuid4().hex[:10]}"
|
|
||||||
response = client.post(
|
|
||||||
"/api/v1/projects",
|
|
||||||
json={"name": name, "year": None, "branch_id": 1},
|
|
||||||
headers=auth_headers(admin_tokens),
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
assert "project_id" in payload
|
|
||||||
assert (project_id := payload["project_id"]) is not None
|
|
||||||
response = client.get(
|
|
||||||
f"/api/v1/projects/{project_id}",
|
|
||||||
headers=auth_headers(admin_tokens),
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
assert "result" in payload
|
|
||||||
assert "id" in payload["result"]
|
|
||||||
assert payload["result"]["id"] == project_id
|
|
||||||
assert "years" in payload["result"]
|
|
||||||
assert isinstance(payload["result"]["years"], list)
|
|
||||||
assert len(payload["result"]["years"]) == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_projects_write_smoke(client, admin_tokens, auth_headers):
|
def test_projects_write_smoke(client, admin_tokens, auth_headers):
|
||||||
project_id, name = _create_project(client, admin_tokens, auth_headers)
|
project_id, name = _create_project(client, admin_tokens, auth_headers)
|
||||||
|
|
||||||
|
|||||||
@ -40,10 +40,10 @@ def test_vsp_dropdown_smoke(client, admin_tokens, auth_headers):
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"params, count",
|
"params, count",
|
||||||
[
|
[
|
||||||
([("ssp_id", 2)], 2),
|
([("ssp_id", 2)], 1),
|
||||||
([("form_id", 1)], 0),
|
([("form_id", 1)], 0),
|
||||||
([("form_id", 2)], 2),
|
([("form_id", 2)], 1),
|
||||||
([("ssp_id", 2), ("form_id", 1)], 2),
|
([("ssp_id", 2), ("form_id", 1)], 1),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
def test_vsp_dropdown_with_filter(client, admin_tokens, auth_headers, params, count):
|
def test_vsp_dropdown_with_filter(client, admin_tokens, auth_headers, params, count):
|
||||||
@ -110,8 +110,8 @@ def test_vsp_create_with_all_fields(client, admin_tokens, auth_headers):
|
|||||||
"close_date": "2025-12-31",
|
"close_date": "2025-12-31",
|
||||||
"approved_format": "Стандарт",
|
"approved_format": "Стандарт",
|
||||||
"notes": "Тестовое примечание",
|
"notes": "Тестовое примечание",
|
||||||
"placement_type": "Аренда",
|
"location_form": "Встроенное",
|
||||||
"staff_count": 10,
|
"numbers": 10,
|
||||||
"area": 150.5,
|
"area": 150.5,
|
||||||
"rent_contract_num": "Д-123/24",
|
"rent_contract_num": "Д-123/24",
|
||||||
"rent_end_date": "2025-12-31",
|
"rent_end_date": "2025-12-31",
|
||||||
@ -127,8 +127,8 @@ def test_vsp_create_with_all_fields(client, admin_tokens, auth_headers):
|
|||||||
assert payload["result"]["open_date"] == "2024-01-15"
|
assert payload["result"]["open_date"] == "2024-01-15"
|
||||||
assert payload["result"]["close_date"] == "2025-12-31"
|
assert payload["result"]["close_date"] == "2025-12-31"
|
||||||
assert payload["result"]["approved_format"] == "Стандарт"
|
assert payload["result"]["approved_format"] == "Стандарт"
|
||||||
assert payload["result"]["placement_type"] == "Аренда"
|
assert payload["result"]["location_form"] == "Встроенное"
|
||||||
assert payload["result"]["staff_count"] == 10
|
assert payload["result"]["numbers"] == 10
|
||||||
assert payload["result"]["area"] == 150.5
|
assert payload["result"]["area"] == 150.5
|
||||||
assert payload["result"]["rent_contract_num"] == "Д-123/24"
|
assert payload["result"]["rent_contract_num"] == "Д-123/24"
|
||||||
assert payload["result"]["rent_end_date"] == "2025-12-31"
|
assert payload["result"]["rent_end_date"] == "2025-12-31"
|
||||||
@ -150,36 +150,6 @@ def test_vsp_create_forbidden_for_executor(client, isp_tokens, auth_headers):
|
|||||||
assert "message" in payload
|
assert "message" in payload
|
||||||
|
|
||||||
|
|
||||||
def test_vsp_create_with_inactive_ssp_fails(client, admin_tokens, auth_headers):
|
|
||||||
created_org = client.post(
|
|
||||||
"/api/v1/org-unit/",
|
|
||||||
json={"title": f"Inactive SSP {uuid.uuid4().hex[:6]}", "is_ssp": True},
|
|
||||||
headers=auth_headers(admin_tokens),
|
|
||||||
)
|
|
||||||
assert created_org.status_code == 200
|
|
||||||
org_unit_id = created_org.json()["result"]["id"]
|
|
||||||
|
|
||||||
deactivate = client.patch(
|
|
||||||
f"/api/v1/org-unit/{org_unit_id}",
|
|
||||||
json={"is_active": False},
|
|
||||||
headers=auth_headers(admin_tokens),
|
|
||||||
)
|
|
||||||
assert deactivate.status_code == 200
|
|
||||||
|
|
||||||
response = client.post(
|
|
||||||
"/api/v1/dict/info",
|
|
||||||
json={
|
|
||||||
"registration_number": _unique_reg_number(),
|
|
||||||
"address": "Should Fail",
|
|
||||||
"ssp_id": org_unit_id,
|
|
||||||
},
|
|
||||||
headers=auth_headers(admin_tokens),
|
|
||||||
)
|
|
||||||
assert response.status_code == 400
|
|
||||||
payload = response.json()
|
|
||||||
assert "message" in payload
|
|
||||||
|
|
||||||
|
|
||||||
def test_vsp_update_smoke(client, admin_tokens, auth_headers):
|
def test_vsp_update_smoke(client, admin_tokens, auth_headers):
|
||||||
reg_number = _unique_reg_number()
|
reg_number = _unique_reg_number()
|
||||||
created = client.post(
|
created = client.post(
|
||||||
@ -225,33 +195,7 @@ def test_vsp_delete_smoke(client, admin_tokens, auth_headers):
|
|||||||
json={
|
json={
|
||||||
"registration_number": reg_number,
|
"registration_number": reg_number,
|
||||||
"address": "To Be Deleted",
|
"address": "To Be Deleted",
|
||||||
"ssp_id": 2,
|
"ssp_id": 1,
|
||||||
},
|
|
||||||
headers=auth_headers(admin_tokens),
|
|
||||||
)
|
|
||||||
assert created.status_code == 201
|
|
||||||
vsp_id = created.json()["result"]["id"]
|
|
||||||
|
|
||||||
response = client.delete(
|
|
||||||
f"/api/v1/dict/info/{vsp_id}",
|
|
||||||
headers=auth_headers(admin_tokens),
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
assert payload["success"] is True
|
|
||||||
assert payload["message"] == "Запись INFO удалена"
|
|
||||||
|
|
||||||
|
|
||||||
def test_vsp_delete_with_close_date_smoke(client, admin_tokens, auth_headers):
|
|
||||||
reg_number = _unique_reg_number()
|
|
||||||
created = client.post(
|
|
||||||
"/api/v1/dict/info",
|
|
||||||
json={
|
|
||||||
"registration_number": reg_number,
|
|
||||||
"address": "To Be Deleted with close_date",
|
|
||||||
"ssp_id": 2,
|
|
||||||
"open_date": "2026-01-01",
|
|
||||||
"close_date": "2030-01-01",
|
|
||||||
},
|
},
|
||||||
headers=auth_headers(admin_tokens),
|
headers=auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,9 +3,9 @@ INSERT INTO v3.app_user (id,email,username,hashed_password,full_name,role_id,is_
|
|||||||
(2,'ispolnitel@rshb.ru','isp1','{isp_password}','Роль Исполнитель Первый',2,true,'2026-05-20 11:00:51.811967+03','2026-06-17 11:51:01.123054+03');
|
(2,'ispolnitel@rshb.ru','isp1','{isp_password}','Роль Исполнитель Первый',2,true,'2026-05-20 11:00:51.811967+03','2026-06-17 11:51:01.123054+03');
|
||||||
SELECT setval('v3.app_user_id_seq', 2);
|
SELECT setval('v3.app_user_id_seq', 2);
|
||||||
|
|
||||||
INSERT INTO v3.org_unit (id,title,is_active,is_ssp,utc_offset) VALUES
|
INSERT INTO v3.org_unit (id,title,is_active,is_ssp) VALUES
|
||||||
(1,'Test_SSP',true,true,'+03'),
|
(1,'Test_SSP',true,true),
|
||||||
(2,'Test РФ',true,false,'+05');
|
(2,'Test РФ',true,false);
|
||||||
SELECT setval('v3.org_unit_id_seq', 2);
|
SELECT setval('v3.org_unit_id_seq', 2);
|
||||||
|
|
||||||
INSERT INTO v3.user_org (id,user_id,org_unit_id) VALUES
|
INSERT INTO v3.user_org (id,user_id,org_unit_id) VALUES
|
||||||
@ -24,24 +24,9 @@ INSERT INTO v3.budget_line (id,budget_form_id,expense_item_id,"name",internal_or
|
|||||||
SELECT setval('v3.budget_line_id_seq', 1);
|
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');
|
||||||
(4,'AHR','test','DFIP','{{plan.q1}}','2029-05-05 03:00:00+05','2029-06-06 03:00:00+05');
|
|
||||||
|
|
||||||
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
|
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,numbers,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,'',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','2027-10-02'),
|
(2,2,'Test12 -----','Тестовая 125','формат 15 ','2026-06-08',NULL,NULL,58,NULL,true,'2026-06-16 18:53:17.986651',false,'2026-06-09 09:04:33.29858',91,'123-1234',91,'субаренда','Работает ','аренда',155,'12-45','2026-10-02');
|
||||||
(3,2,'3434','победы 12','формат','2026-06-01','субаренда', NULL,34,NULL,true,'2026-07-01 13:32:35.327097',false,'2026-06-29 15:53:50.236062',91,'3434',NULL,'2','примечание','567','2027-07-01');
|
SELECT setval('v3.vsp_id_seq', 2);
|
||||||
SELECT setval('v3.vsp_id_seq', 3);
|
|
||||||
|
|
||||||
INSERT INTO v3.project ("id","name","level",parent_id,project_type,vsp_format,placement_type,object_address,staff_count,total_area,org_unit_id) VALUES
|
|
||||||
(2,'PT6d112310_U','project',NULL,NULL,NULL,NULL,NULL,NULL,NULL,1);
|
|
||||||
SELECT setval('v3.project_id_seq', 3);
|
|
||||||
|
|
||||||
INSERT INTO v3.rf_project_report (id,project_id,"year",report_type,created_by,created_at,updated_by,updated_at) VALUES
|
|
||||||
(1,2,2026,'LIMIT',NULL,'2026-05-12 12:43:56.20138',NULL,'2026-05-12 12:43:56.20138'),
|
|
||||||
(2,2,2026,'CURRENT_EXPENSES',NULL,'2026-05-12 12:43:56.20138',NULL,'2026-05-12 12:43:56.20138');
|
|
||||||
SELECT setval('v3.rf_project_report_id_seq', 3);
|
|
||||||
|
|
||||||
INSERT INTO v3.form3_phase (rf_project_report_id,phase_code,"role",column_keys,opens_at,closes_at) VALUES
|
|
||||||
(1,'RF_FILL_2026','EXECUTOR_RF','{{q1.adj_by_items,q1.adj_increase,q1.m1,q1.m2,q1.m3,q2.adj_by_items,q2.adj_increase,q2.m1,q2.m2,q2.m3,q3.adj_by_items,q3.adj_increase,q3.m1,q3.m2,q3.m3,q4.adj_by_items,q4.adj_increase,q4.m1,q4.m2,q4.m3,q4.spod}}','2030-01-01 03:00:00+03','2031-01-01 02:59:59+03'),
|
|
||||||
(2,'RF_FILL_2026','EXECUTOR_RF','{{q1.adj_by_items,q1.adj_increase,q1.m1,q1.m2,q1.m3,q2.adj_by_items,q2.adj_increase,q2.m1,q2.m2,q2.m3,q3.adj_by_items,q3.adj_increase,q3.m1,q3.m2,q3.m3,q4.adj_by_items,q4.adj_increase,q4.m1,q4.m2,q4.m3,q4.spod}}','2030-01-01 03:00:00+03','2031-01-01 02:59:59+03');
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export const DictVspApi = {
|
|||||||
responseType: "blob",
|
responseType: "blob",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getDropdownVsp(params) {
|
getDropdownVsp(params){
|
||||||
return api.get(`/dict/info/dropdown`, { params }).then((r) => r.data);
|
return api.get(`/dict/info/dropdown`, params).then((r) => r.data);
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
import api from "./client";
|
|
||||||
|
|
||||||
export const ProjectsApi = {
|
|
||||||
list: (params) => {
|
|
||||||
return api.get(`/projects`, { params }).then((r) => r.data);
|
|
||||||
},
|
|
||||||
|
|
||||||
get: (id) => {
|
|
||||||
return api.get(`/projects/${id}`).then((r) => r.data);
|
|
||||||
},
|
|
||||||
|
|
||||||
create: (data) => {
|
|
||||||
return api.post("/projects", data).then((r) => r.data);
|
|
||||||
},
|
|
||||||
|
|
||||||
update: (id, data) => {
|
|
||||||
return api.patch(`/projects/${id}`, data).then((r) => r.data);
|
|
||||||
},
|
|
||||||
|
|
||||||
delete: (id) => {
|
|
||||||
return api.delete(`/projects/${id}`).then((r) => r.data);
|
|
||||||
},
|
|
||||||
|
|
||||||
getTableData: (id, sheetName, year) => {
|
|
||||||
return api
|
|
||||||
.get(`/projects/${id}/report/${year}/${sheetName}`)
|
|
||||||
.then((r) => r.data);
|
|
||||||
},
|
|
||||||
export: (id, year, sheetName) => {
|
|
||||||
return api.get(`/export/project/${id}/report/${year}/${sheetName}`, {
|
|
||||||
responseType: "blob",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -17,7 +17,6 @@ import { CircularProgress } from '@mui/material';
|
|||||||
import NewTablePage from '../pages/NewTablePage.jsx';
|
import NewTablePage from '../pages/NewTablePage.jsx';
|
||||||
import TablesTest from '../pages/TablesTest.jsx';
|
import TablesTest from '../pages/TablesTest.jsx';
|
||||||
import NewFormTablePage from '../pages/NewFormTablePage.jsx'
|
import NewFormTablePage from '../pages/NewFormTablePage.jsx'
|
||||||
import ProjectsPage from '../pages/ProjectsPage/ProjectsPage.jsx';
|
|
||||||
|
|
||||||
export const PrivateRoute = () => {
|
export const PrivateRoute = () => {
|
||||||
const { isAuthenticated, loading } = useAuth();
|
const { isAuthenticated, loading } = useAuth();
|
||||||
@ -38,10 +37,8 @@ export const AppRoutes = () => {
|
|||||||
<Route element={<PrivateRoute />}>
|
<Route element={<PrivateRoute />}>
|
||||||
<Route path="/" element={<TasksPage />} />
|
<Route path="/" element={<TasksPage />} />
|
||||||
<Route path="/tasks" element={<TasksPage />} />
|
<Route path="/tasks" element={<TasksPage />} />
|
||||||
<Route path="/projects" element={<ProjectsPage />}/>
|
|
||||||
<Route path="/forms" element={<FormsPage />} />
|
<Route path="/forms" element={<FormsPage />} />
|
||||||
<Route path="/task/:taskId" element={<TaskPage />} />
|
<Route path="/task/:taskId" element={<TaskPage />} />
|
||||||
<Route path="/project/:projectId" element={<TaskPage />} />
|
|
||||||
<Route path="/table/:tableId" element={<TablePage />} />
|
<Route path="/table/:tableId" element={<TablePage />} />
|
||||||
<Route path="/table-temp/:tableId" element={<TableTempPage />} />
|
<Route path="/table-temp/:tableId" element={<TableTempPage />} />
|
||||||
<Route path="/admin_panel/users" element={<UsersPage />} />
|
<Route path="/admin_panel/users" element={<UsersPage />} />
|
||||||
@ -52,7 +49,7 @@ export const AppRoutes = () => {
|
|||||||
<Route path="/new-table" element={<NewTablePage />} />
|
<Route path="/new-table" element={<NewTablePage />} />
|
||||||
<Route path="/tables-new" element={<TablesTest />} />
|
<Route path="/tables-new" element={<TablesTest />} />
|
||||||
<Route path="/tables/:form/:sheetName" element={<NewFormTablePage />} />
|
<Route path="/tables/:form/:sheetName" element={<NewFormTablePage />} />
|
||||||
<Route path="/table/form/:formId/form-type/:formType/:sheetName/:direction/:year" element={<NewFormTablePage />} />
|
<Route path="/table/form/:formId/form-type/:formType/:sheetName/:direction" element={<NewFormTablePage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { NavLink } from 'react-router-dom';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { TasksSvg, AdminPanelSvg, DictSvg } from '../common/icons/icons';
|
import { TasksSvg, AdminPanelSvg, DictSvg } from '../common/icons/icons';
|
||||||
import { useAuth } from '../../app/context/AuthProvider';
|
import { useAuth } from '../../app/context/AuthProvider';
|
||||||
import { ROLES_NAME_ID } from '../../constants/constants';
|
import { ROLES_NAME_ID } from '../../constants';
|
||||||
|
|
||||||
const Switch = styled.div`
|
const Switch = styled.div`
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
|
|||||||
@ -1,205 +1,144 @@
|
|||||||
import React, { memo, useMemo, useCallback } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
import { useRealtime } from '../../contexts/RealtimeContext';
|
import { useRealtime } from '../../contexts/RealtimeContext';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
// Константы вне компонента
|
const ContainerForText = styled.span`
|
||||||
const BASE_CELL_STYLES = {
|
display: -webkit-box;
|
||||||
width: '100%',
|
-webkit-box-orient: vertical;
|
||||||
height: '100%',
|
-webkit-line-clamp: 3;
|
||||||
display: 'flex',
|
overflow: hidden;
|
||||||
alignItems: 'center',
|
white-space: pre-wrap;
|
||||||
justifyContent: 'center',
|
`;
|
||||||
boxSizing: 'border-box',
|
|
||||||
position: 'absolute',
|
// Стилизованный компонент для плашки блокировки
|
||||||
top: 0,
|
const LockBadge = styled.div`
|
||||||
left: 0,
|
position: absolute;
|
||||||
right: 0,
|
top: 2px;
|
||||||
bottom: 0,
|
right: 2px;
|
||||||
border: '2px solid transparent',
|
background: rgba(0, 0, 0, 0.7);
|
||||||
padding: '8px',
|
color: white;
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Функция для подсветки текста
|
||||||
|
const highlightText = (text, searchQuery) => {
|
||||||
|
if (!searchQuery || !text) return text;
|
||||||
|
|
||||||
|
const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
const regex = new RegExp(`(${escapedQuery})`, 'gi');
|
||||||
|
|
||||||
|
const parts = text.split(regex);
|
||||||
|
|
||||||
|
return parts.map((part, index) =>
|
||||||
|
regex.test(part) ? (
|
||||||
|
<mark
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#ffeb3b',
|
||||||
|
color: '#000',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
padding: '0 2px',
|
||||||
|
borderRadius: '2px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{part}
|
||||||
|
</mark>
|
||||||
|
) : (
|
||||||
|
part
|
||||||
|
),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const LOCKED_STYLES = {
|
// Функция для форматирования числа в финансовый формат
|
||||||
border: '2px solid #e0e0e0',
|
const formatNumber = (value) => {
|
||||||
backgroundColor: '#f5f5f5',
|
|
||||||
cursor: 'not-allowed',
|
|
||||||
opacity: 0.85,
|
|
||||||
};
|
|
||||||
|
|
||||||
const LOCK_BADGE_STYLES = {
|
|
||||||
position: 'absolute',
|
|
||||||
top: '2px',
|
|
||||||
right: '2px',
|
|
||||||
background: 'rgba(0, 0, 0, 0.7)',
|
|
||||||
color: 'white',
|
|
||||||
fontSize: '9px',
|
|
||||||
padding: '1px 4px',
|
|
||||||
borderRadius: '3px',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
zIndex: 10,
|
|
||||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
||||||
letterSpacing: '0.3px',
|
|
||||||
backdropFilter: 'blur(4px)',
|
|
||||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
||||||
};
|
|
||||||
|
|
||||||
const CONTAINER_STYLES = {
|
|
||||||
display: '-webkit-box',
|
|
||||||
WebkitBoxOrient: 'vertical',
|
|
||||||
WebkitLineClamp: 3,
|
|
||||||
overflow: 'hidden',
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
};
|
|
||||||
|
|
||||||
const INTEGER_COLUMN_IDS = new Set([
|
|
||||||
'data.header.num_group',
|
|
||||||
'data.nomenclature_group_id',
|
|
||||||
'data.header.item_id',
|
|
||||||
'data.article_id',
|
|
||||||
'data.code_razdela',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const getColorBrightness = (hexColor) => {
|
|
||||||
if (!hexColor) return 255;
|
|
||||||
const color = hexColor.replace('#', '');
|
|
||||||
let r, g, b;
|
|
||||||
if (color.length === 3) {
|
|
||||||
r = parseInt(color[0] + color[0], 16);
|
|
||||||
g = parseInt(color[1] + color[1], 16);
|
|
||||||
b = parseInt(color[2] + color[2], 16);
|
|
||||||
} else if (color.length === 6) {
|
|
||||||
r = parseInt(color.substring(0, 2), 16);
|
|
||||||
g = parseInt(color.substring(2, 4), 16);
|
|
||||||
b = parseInt(color.substring(4, 6), 16);
|
|
||||||
} else {
|
|
||||||
return 255;
|
|
||||||
}
|
|
||||||
return (0.299 * r + 0.587 * g + 0.114 * b);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatNumber = (value, asInteger = false) => {
|
|
||||||
if (value === null || value === undefined || value === '') return String(value || '');
|
if (value === null || value === undefined || value === '') return String(value || '');
|
||||||
|
|
||||||
const num = Number(value);
|
const num = Number(value);
|
||||||
if (isNaN(num)) return String(value);
|
if (isNaN(num)) return String(value);
|
||||||
|
|
||||||
return num.toLocaleString('ru-RU', {
|
return num.toLocaleString('ru-RU', {
|
||||||
minimumFractionDigits: asInteger ? 0 : 1,
|
minimumFractionDigits: 1,
|
||||||
maximumFractionDigits: asInteger ? 0 : 1,
|
maximumFractionDigits: 1,
|
||||||
useGrouping: true,
|
useGrouping: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Оптимизированная функция подсветки
|
const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundColor, color, isEditable }) => {
|
||||||
const createHighlightedContent = (originalValue, displayValue, searchQueries) => {
|
|
||||||
const cleanQueries = searchQueries.filter(Boolean);
|
|
||||||
if (cleanQueries.length === 0) return displayValue;
|
|
||||||
|
|
||||||
const searchStr = String(originalValue ?? displayValue);
|
|
||||||
const escapedQueries = cleanQueries.map(q => q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
||||||
const regex = new RegExp(`(${escapedQueries.join('|')})`, 'gi');
|
|
||||||
|
|
||||||
if (!regex.test(searchStr)) return displayValue;
|
|
||||||
|
|
||||||
const parts = searchStr.split(regex);
|
|
||||||
return parts.map((part, index) => {
|
|
||||||
if (!regex.test(part)) return part;
|
|
||||||
return React.createElement('mark', {
|
|
||||||
key: index,
|
|
||||||
style: {
|
|
||||||
backgroundColor: '#ffeb3b',
|
|
||||||
color: '#000',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
padding: '0 2px',
|
|
||||||
borderRadius: '2px',
|
|
||||||
},
|
|
||||||
}, part);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const CellComponent = ({
|
|
||||||
cell,
|
|
||||||
row,
|
|
||||||
column,
|
|
||||||
onClick,
|
|
||||||
globalFilter,
|
|
||||||
columnFilter,
|
|
||||||
backgroundColor,
|
|
||||||
color,
|
|
||||||
isEditable,
|
|
||||||
isUpdating,
|
|
||||||
onCellNumberClick,
|
|
||||||
}) => {
|
|
||||||
const { lockedCells } = useRealtime();
|
const { lockedCells } = useRealtime();
|
||||||
const cellKey = `${row.id}_${column.id}`;
|
const cellKey = `${row.id}_${column.id}`;
|
||||||
|
|
||||||
const isLocked = lockedCells.includes(cellKey);
|
const isLocked = lockedCells.includes(cellKey);
|
||||||
|
|
||||||
// Мемоизация значения
|
const cellValue = cell.getValue();
|
||||||
const { rawValue, displayValue, isNumeric } = useMemo(() => {
|
let textValue = String(cellValue || '');
|
||||||
const value = cell.getValue();
|
|
||||||
const isNum = !isNaN(Number(value)) && value !== null && value !== undefined && value !== '';
|
|
||||||
let display = String(value || '');
|
|
||||||
|
|
||||||
if (isNum) {
|
const isNumeric = !isNaN(Number(cellValue)) && cellValue !== null && cellValue !== undefined && cellValue !== '';
|
||||||
const asInteger = INTEGER_COLUMN_IDS.has(column.id);
|
if (isNumeric) {
|
||||||
display = formatNumber(value, asInteger);
|
textValue = formatNumber(cellValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { rawValue: value, displayValue: display, isNumeric: isNum };
|
|
||||||
}, [cell, column.id]);
|
|
||||||
|
|
||||||
const highlightedContent = useMemo(() => {
|
const highlightedContent = useMemo(() => {
|
||||||
const searchQueries = [globalFilter, columnFilter];
|
return highlightText(textValue, globalFilter);
|
||||||
return createHighlightedContent(rawValue, displayValue, searchQueries);
|
}, [textValue, globalFilter]);
|
||||||
}, [rawValue, displayValue, globalFilter, columnFilter]);
|
|
||||||
|
|
||||||
const textColor = useMemo(() => {
|
// Базовые стили для заблокированной ячейки
|
||||||
if (isLocked) return '#999999';
|
const lockedStyles = isLocked ? {
|
||||||
const brightness = getColorBrightness(backgroundColor);
|
border: '2px solid #e0e0e0',
|
||||||
return brightness < 128 ? '#ffffff' : '#000000';
|
backgroundColor: '#f5f5f5',
|
||||||
}, [backgroundColor, isLocked]);
|
color: '#999999',
|
||||||
|
cursor: 'not-allowed',
|
||||||
|
opacity: 0.85,
|
||||||
|
} : {};
|
||||||
|
|
||||||
const cellStyles = useMemo(() => ({
|
// Если isEditable false, добавляем дополнительные стили
|
||||||
...BASE_CELL_STYLES,
|
const editableStyles = !isEditable ? {
|
||||||
backgroundColor: isLocked ? '#f5f5f5' : backgroundColor,
|
border: '2px solid #d3d3d3',
|
||||||
color: textColor,
|
color: backgroundColor?.toLowerCase() === '#933634' ? '#ffffff' : '#525252',
|
||||||
...(isLocked ? LOCKED_STYLES : {}),
|
cursor: 'not-allowed',
|
||||||
}), [backgroundColor, isLocked, textColor]);
|
} : {};
|
||||||
|
|
||||||
const lockBadge = useMemo(() => {
|
|
||||||
if (!isLocked) return null;
|
|
||||||
return <div style={LOCK_BADGE_STYLES}>🔒</div>;
|
|
||||||
}, [isLocked]);
|
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
|
||||||
if (!isLocked && onClick) {
|
|
||||||
onClick();
|
|
||||||
}
|
|
||||||
}, [isLocked, onClick]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="cell"
|
className="cell"
|
||||||
style={cellStyles}
|
style={{
|
||||||
onClick={handleClick}
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
border: '2px solid transparent',
|
||||||
|
padding: '8px',
|
||||||
|
backgroundColor: isLocked ? '#f5f5f5' : backgroundColor,
|
||||||
|
color: isLocked ? '#999999' : color,
|
||||||
|
...lockedStyles,
|
||||||
|
...editableStyles,
|
||||||
|
}}
|
||||||
|
onClick={isLocked ? undefined : onClick}
|
||||||
>
|
>
|
||||||
<span style={CONTAINER_STYLES}>{highlightedContent}</span>
|
<ContainerForText>{highlightedContent}</ContainerForText>
|
||||||
{lockBadge}
|
{isLocked && (
|
||||||
|
<LockBadge>
|
||||||
|
🔒
|
||||||
|
</LockBadge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const Cell = React.memo(CellComponent, (prevProps, nextProps) => {
|
|
||||||
// Возвращаем true если пропсы равны (не нужно перерендеривать)
|
|
||||||
return (
|
|
||||||
prevProps.cell.getValue() === nextProps.cell.getValue() &&
|
|
||||||
prevProps.row.id === nextProps.row.id &&
|
|
||||||
prevProps.column.id === nextProps.column.id &&
|
|
||||||
prevProps.globalFilter === nextProps.globalFilter &&
|
|
||||||
prevProps.columnFilter === nextProps.columnFilter &&
|
|
||||||
prevProps.backgroundColor === nextProps.backgroundColor &&
|
|
||||||
prevProps.isEditable === nextProps.isEditable &&
|
|
||||||
prevProps.isUpdating === nextProps.isUpdating &&
|
|
||||||
prevProps.onCellNumberClick === nextProps.onCellNumberClick
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Cell;
|
export default Cell;
|
||||||
@ -1,217 +1,173 @@
|
|||||||
import React, { memo, useEffect, useRef, useLayoutEffect, useState, useCallback, useMemo } from 'react';
|
// EditCell.js - обновленная версия
|
||||||
|
import { useEffect, useRef, useLayoutEffect, useState } from 'react';
|
||||||
import { isFormula, extractFormula, calculateFormula, formatNumber } from '../../utils/formulaUtils';
|
import { isFormula, extractFormula, calculateFormula, formatNumber } from '../../utils/formulaUtils';
|
||||||
import { saveToStorage, loadFromStorage } from '../../utils/localStorageUtils';
|
import { saveToStorage, loadFromStorage } from '../../utils/localStorageUtils';
|
||||||
import { useRealtime } from '../../contexts/RealtimeContext';
|
|
||||||
|
|
||||||
const EDITOR_STYLES = {
|
const EditCell = ({ refCell, onChange, disabled, value, tableId, cellId }) => {
|
||||||
position: 'absolute',
|
const [curValue, setCurValue] = useState(value);
|
||||||
zIndex: 12,
|
const [displayValue, setDisplayValue] = useState(value);
|
||||||
top: 0,
|
const [isFormulaMode, setIsFormulaMode] = useState(false);
|
||||||
};
|
|
||||||
|
|
||||||
const FORMULA_INDICATOR_STYLES = {
|
// Ключ для хранения формул в localStorage
|
||||||
position: 'absolute',
|
|
||||||
top: '-20px',
|
|
||||||
left: '4px',
|
|
||||||
fontSize: '10px',
|
|
||||||
color: '#666',
|
|
||||||
background: '#f0f0f0',
|
|
||||||
padding: '2px 6px',
|
|
||||||
borderRadius: '3px',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
};
|
|
||||||
|
|
||||||
const TEXTAREA_STYLES = {
|
|
||||||
width: '100%',
|
|
||||||
height: '3rem',
|
|
||||||
fieldSizing: 'content',
|
|
||||||
fontSize: 'inherit',
|
|
||||||
};
|
|
||||||
|
|
||||||
const EditCell = memo(({ refCell, onChange, disabled, value, tableId, cellId, table }) => {
|
|
||||||
const { endEditing: contextEndEditing } = useRealtime();
|
|
||||||
const storageKey = `formula_${tableId}_${cellId}`;
|
const storageKey = `formula_${tableId}_${cellId}`;
|
||||||
|
|
||||||
const [state, setState] = useState(() => ({
|
const trFrag = refCell.current.offsetParent.offsetParent;
|
||||||
currentValue: value,
|
const tdFrag = refCell.current.offsetParent;
|
||||||
displayValue: value,
|
const left = tdFrag.offsetLeft;
|
||||||
isFormulaMode: false,
|
const width = refCell.current.offsetParent.offsetWidth;
|
||||||
}));
|
const translateY = trFrag.style.transform;
|
||||||
|
|
||||||
// Позиция ячейки
|
// Загружаем сохраненную формулу при монтировании
|
||||||
const [position, setPosition] = useState();
|
|
||||||
const textareaRef = useRef(null);
|
|
||||||
|
|
||||||
const refFinished = useRef(false);
|
|
||||||
|
|
||||||
// Загрузка сохраненной формулы
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedFormula = loadFromStorage(storageKey);
|
const savedFormula = loadFromStorage(storageKey);
|
||||||
if (savedFormula && isFormula(savedFormula)) {
|
if (savedFormula && isFormula(savedFormula)) {
|
||||||
|
setIsFormulaMode(true);
|
||||||
|
setCurValue(savedFormula);
|
||||||
|
// Вычисляем и отображаем результат
|
||||||
try {
|
try {
|
||||||
const formula = extractFormula(savedFormula);
|
const formula = extractFormula(savedFormula);
|
||||||
const result = calculateFormula(formula);
|
const result = calculateFormula(formula);
|
||||||
setState({
|
setDisplayValue(formatNumber(result));
|
||||||
currentValue: savedFormula,
|
} catch (error) {
|
||||||
displayValue: formatNumber(result),
|
setDisplayValue('#ОШИБКА');
|
||||||
isFormulaMode: true,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
setState({
|
|
||||||
currentValue: savedFormula,
|
|
||||||
displayValue: '#ОШИБКА',
|
|
||||||
isFormulaMode: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [storageKey]);
|
|
||||||
|
|
||||||
// Вычисление позиции
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (!refCell?.current) return;
|
|
||||||
|
|
||||||
const td = refCell.current.offsetParent;
|
|
||||||
const tr = td?.offsetParent;
|
|
||||||
|
|
||||||
if (td && tr) {
|
|
||||||
setPosition({
|
|
||||||
left: td.offsetLeft,
|
|
||||||
width: td.offsetWidth,
|
|
||||||
translateY: tr.style.transform || '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [refCell]);
|
|
||||||
|
|
||||||
// Auto-resize
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const el = textareaRef.current;
|
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
const resize = () => {
|
|
||||||
el.style.minHeight = '3rem';
|
|
||||||
el.style.height = '3rem';
|
|
||||||
el.style.height = el.scrollHeight + 'px';
|
|
||||||
el.style.maxHeight = '180px';
|
|
||||||
};
|
|
||||||
|
|
||||||
resize();
|
|
||||||
const observer = new ResizeObserver(resize);
|
|
||||||
observer.observe(el);
|
|
||||||
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, [state.currentValue]);
|
|
||||||
|
|
||||||
const handleSave = useCallback(() => {
|
|
||||||
const { currentValue } = state;
|
|
||||||
|
|
||||||
if (isFormula(currentValue)) {
|
|
||||||
saveToStorage(storageKey, currentValue);
|
|
||||||
try {
|
|
||||||
const formula = extractFormula(currentValue);
|
|
||||||
const result = calculateFormula(formula);
|
|
||||||
const formattedResult = formatNumber(result);
|
|
||||||
setState(prev => ({
|
|
||||||
...prev,
|
|
||||||
displayValue: formattedResult,
|
|
||||||
isFormulaMode: true,
|
|
||||||
}));
|
|
||||||
onChange?.(formattedResult);
|
|
||||||
} catch {
|
|
||||||
setState(prev => ({
|
|
||||||
...prev,
|
|
||||||
displayValue: '#ОШИБКА',
|
|
||||||
isFormulaMode: true,
|
|
||||||
}));
|
|
||||||
onChange?.(currentValue);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem(storageKey);
|
setDisplayValue(value);
|
||||||
setState(prev => ({
|
|
||||||
...prev,
|
|
||||||
displayValue: currentValue,
|
|
||||||
isFormulaMode: false,
|
|
||||||
}));
|
|
||||||
onChange?.(currentValue);
|
|
||||||
}
|
}
|
||||||
}, [state, storageKey, onChange]);
|
}, [storageKey, value]);
|
||||||
|
|
||||||
const finishEditing = useCallback(() => {
|
function useAutoResize(value) {
|
||||||
if (refFinished.current) return;
|
const ref = useRef(null);
|
||||||
refFinished.current = true;
|
|
||||||
if (!disabled) {
|
useLayoutEffect(() => {
|
||||||
handleSave();
|
const el = ref.current;
|
||||||
}
|
if (!el) return;
|
||||||
const editingCell = table.getState().editingCell;
|
|
||||||
if (editingCell) {
|
el.style.height = 'auto';
|
||||||
contextEndEditing?.(editingCell.row, editingCell.column);
|
el.style.minHeight = '3rem';
|
||||||
}
|
el.style.height = el.scrollHeight + 'px';
|
||||||
}, [disabled, handleSave, table, contextEndEditing]);
|
el.style.maxHeight = '180px';
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ref = useAutoResize(value);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event) => {
|
if (ref.current && !disabled) {
|
||||||
const el = textareaRef.current;
|
ref.current.focus();
|
||||||
if (el && !el.contains(event.target)) {
|
ref.current.setSelectionRange(
|
||||||
finishEditing();
|
ref.current.value.length,
|
||||||
}
|
ref.current.value.length,
|
||||||
};
|
);
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
}, [finishEditing]);
|
|
||||||
|
|
||||||
const handleChange = useCallback((e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
setState(prev => ({
|
|
||||||
...prev,
|
|
||||||
currentValue: value,
|
|
||||||
displayValue: value,
|
|
||||||
}));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
finishEditing();
|
|
||||||
}
|
}
|
||||||
}, [finishEditing]);
|
}, [disabled, ref]);
|
||||||
|
|
||||||
const editorStyles = useMemo(() => ({
|
const handleSave = () => {
|
||||||
...EDITOR_STYLES,
|
// Проверяем, является ли введенное значение формулой
|
||||||
left: position?.left || 0,
|
if (isFormula(curValue)) {
|
||||||
width: position?.width || 0,
|
// Сохраняем формулу в localStorage
|
||||||
transform: position?.translateY || 0,
|
saveToStorage(storageKey, curValue);
|
||||||
}), [position]);
|
|
||||||
|
|
||||||
const textareaStyles = useMemo(() => ({
|
// Вычисляем и сохраняем результат
|
||||||
...TEXTAREA_STYLES,
|
try {
|
||||||
backgroundColor: state.isFormulaMode ? '#f8f9fa' : 'white',
|
const formula = extractFormula(curValue);
|
||||||
border: state.isFormulaMode ? '2px solid #4a90d9' : undefined,
|
const result = calculateFormula(formula);
|
||||||
}), [state.isFormulaMode]);
|
const formattedResult = formatNumber(result);
|
||||||
|
setDisplayValue(formattedResult);
|
||||||
|
onChange?.(formattedResult);
|
||||||
|
setIsFormulaMode(true);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
setDisplayValue('#ОШИБКА');
|
||||||
|
onChange?.(curValue); // Сохраняем как текст, если ошибка
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Если это не формула, удаляем сохраненную формулу
|
||||||
|
localStorage.removeItem(storageKey);
|
||||||
|
setIsFormulaMode(false);
|
||||||
|
setDisplayValue(curValue);
|
||||||
|
onChange?.(curValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeValue = (event) => {
|
||||||
|
const input = event.target.value;
|
||||||
|
setCurValue(input);
|
||||||
|
// При редактировании показываем введенный текст
|
||||||
|
setDisplayValue(input);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для отображения значения в ячейке
|
||||||
|
const getDisplayValue = () => {
|
||||||
|
if (isFormulaMode && isFormula(curValue)) {
|
||||||
|
return displayValue;
|
||||||
|
}
|
||||||
|
return displayValue;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{position &&
|
<div
|
||||||
<div style={editorStyles}>
|
key="blockForEdit"
|
||||||
{state.isFormulaMode && (
|
style={{
|
||||||
<div style={FORMULA_INDICATOR_STYLES}>fx</div>
|
position: 'absolute',
|
||||||
)}
|
left: left,
|
||||||
<textarea
|
zIndex: 12,
|
||||||
ref={textareaRef}
|
width: width,
|
||||||
disabled={disabled}
|
transform: translateY,
|
||||||
style={textareaStyles}
|
top: 0,
|
||||||
onChange={handleChange}
|
}}
|
||||||
value={state.currentValue}
|
>
|
||||||
onKeyDown={handleKeyDown}
|
{isFormulaMode && (
|
||||||
placeholder={state.isFormulaMode ? 'Введите формулу (начинается с =)' : ''}
|
<div style={{
|
||||||
/>
|
position: 'absolute',
|
||||||
</div>
|
top: '-20px',
|
||||||
}
|
left: '4px',
|
||||||
|
fontSize: '10px',
|
||||||
|
color: '#666',
|
||||||
|
background: '#f0f0f0',
|
||||||
|
padding: '2px 6px',
|
||||||
|
borderRadius: '3px',
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}}>
|
||||||
|
fx
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<textarea
|
||||||
|
ref={ref}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
fieldSizing: 'content',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
backgroundColor: isFormulaMode ? '#f8f9fa' : 'white',
|
||||||
|
border: isFormulaMode ? '2px solid #4a90d9' : undefined,
|
||||||
|
}}
|
||||||
|
onChange={handleChangeValue}
|
||||||
|
value={curValue}
|
||||||
|
onBlur={handleSave}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSave();
|
||||||
|
// Закрываем редактирование
|
||||||
|
if (onChange) {
|
||||||
|
// Симулируем завершение редактирования
|
||||||
|
document.activeElement?.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={isFormulaMode ? 'Введите формулу (начинается с =)' : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
EditCell.displayName = 'EditCell';
|
|
||||||
|
|
||||||
export default EditCell;
|
export default EditCell;
|
||||||
@ -1,148 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
|
|
||||||
const toLocalCoord = (value, scale) => value / scale;
|
|
||||||
|
|
||||||
const getPinnedClipLeft = (container, overlayParent, columnPinning, scale) => {
|
|
||||||
const pinnedIds = (columnPinning?.left || []).filter(Boolean);
|
|
||||||
if (!pinnedIds.length) return 0;
|
|
||||||
|
|
||||||
const parentRect = overlayParent.getBoundingClientRect();
|
|
||||||
let maxRight = 0;
|
|
||||||
|
|
||||||
for (const id of pinnedIds) {
|
|
||||||
const el = container.querySelector(`[data-col-select-id="${id}"]`);
|
|
||||||
if (!el) continue;
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
maxRight = Math.max(maxRight, rect.right - parentRect.left);
|
|
||||||
}
|
|
||||||
|
|
||||||
return toLocalCoord(maxRight, scale);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOverlayRect = (container, overlayParent, selectedColumnId, columnPinning, scale) => {
|
|
||||||
const columnEl = container.querySelector(
|
|
||||||
`[data-col-select-id="${selectedColumnId}"]`,
|
|
||||||
);
|
|
||||||
if (!columnEl || !overlayParent) return null;
|
|
||||||
|
|
||||||
const bodyCell = container.querySelector(
|
|
||||||
`td[data-col-id="${selectedColumnId}"]`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const parentRect = overlayParent.getBoundingClientRect();
|
|
||||||
const containerRect = container.getBoundingClientRect();
|
|
||||||
const columnRect = columnEl.getBoundingClientRect();
|
|
||||||
const alignRect = bodyCell?.getBoundingClientRect() ?? columnRect;
|
|
||||||
|
|
||||||
let top = toLocalCoord(columnRect.top - parentRect.top, scale);
|
|
||||||
let left = toLocalCoord(alignRect.left - parentRect.left, scale);
|
|
||||||
let width = toLocalCoord(alignRect.width, scale);
|
|
||||||
const height = toLocalCoord(containerRect.bottom - columnRect.top, scale);
|
|
||||||
|
|
||||||
const pinnedIds = (columnPinning?.left || []).filter(Boolean);
|
|
||||||
const isSelectedPinned = pinnedIds.includes(selectedColumnId);
|
|
||||||
|
|
||||||
if (!isSelectedPinned && pinnedIds.length) {
|
|
||||||
const clipLeft = getPinnedClipLeft(container, overlayParent, columnPinning, scale);
|
|
||||||
const overlayRight = left + width;
|
|
||||||
if (overlayRight <= clipLeft) return null;
|
|
||||||
if (left < clipLeft) {
|
|
||||||
width = overlayRight - clipLeft;
|
|
||||||
left = clipLeft;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (width <= 0 || height <= 0) return null;
|
|
||||||
|
|
||||||
return { top, left, width, height };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ColumnSelectionOverlay = ({
|
|
||||||
selectedColumnId,
|
|
||||||
containerRef,
|
|
||||||
scale = 1,
|
|
||||||
columnSizing,
|
|
||||||
columnPinning,
|
|
||||||
showColumnFilters,
|
|
||||||
}) => {
|
|
||||||
const [rect, setRect] = useState(null);
|
|
||||||
const [portalTarget, setPortalTarget] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const container = containerRef.current;
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
const overlayParent = container.parentElement;
|
|
||||||
setPortalTarget(overlayParent);
|
|
||||||
}, [containerRef, selectedColumnId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const container = containerRef.current;
|
|
||||||
const overlayParent = container?.parentElement;
|
|
||||||
if (!selectedColumnId || !container || !overlayParent) {
|
|
||||||
setRect(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateRect = () => {
|
|
||||||
setRect(getOverlayRect(container, overlayParent, selectedColumnId, columnPinning, scale));
|
|
||||||
};
|
|
||||||
|
|
||||||
updateRect();
|
|
||||||
|
|
||||||
const scrollTargets = new Set([container]);
|
|
||||||
let scrollParent = container.parentElement;
|
|
||||||
for (let i = 0; i < 3 && scrollParent; i += 1) {
|
|
||||||
scrollTargets.add(scrollParent);
|
|
||||||
scrollParent = scrollParent.parentElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollTargets.forEach((target) => {
|
|
||||||
target.addEventListener('scroll', updateRect, { passive: true });
|
|
||||||
});
|
|
||||||
window.addEventListener('resize', updateRect);
|
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(updateRect);
|
|
||||||
resizeObserver.observe(container);
|
|
||||||
resizeObserver.observe(overlayParent);
|
|
||||||
|
|
||||||
const header = container.querySelector('.custom-table-header');
|
|
||||||
if (header) {
|
|
||||||
resizeObserver.observe(header);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableBody = container.querySelector('.MuiTableBody-root');
|
|
||||||
if (tableBody) {
|
|
||||||
resizeObserver.observe(tableBody);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
scrollTargets.forEach((target) => {
|
|
||||||
target.removeEventListener('scroll', updateRect);
|
|
||||||
});
|
|
||||||
window.removeEventListener('resize', updateRect);
|
|
||||||
resizeObserver.disconnect();
|
|
||||||
};
|
|
||||||
}, [selectedColumnId, containerRef, scale, columnSizing, columnPinning, showColumnFilters]);
|
|
||||||
|
|
||||||
if (!rect || !portalTarget) return null;
|
|
||||||
|
|
||||||
return createPortal(
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: rect.top,
|
|
||||||
left: rect.left,
|
|
||||||
width: rect.width,
|
|
||||||
height: rect.height,
|
|
||||||
backgroundColor: 'rgba(88, 154, 47, 0.2)',
|
|
||||||
border: '1px solid #589A2F',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
zIndex: 1,
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
portalTarget,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
// components/RealtimeTable/index.js
|
||||||
import React, { useState, useMemo, useRef, useEffect, useCallback, useTransition } from 'react';
|
import React, { useState, useMemo, useRef, useEffect, useCallback, useTransition } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import {
|
import {
|
||||||
@ -10,14 +11,13 @@ import { getTableColumns } from './tableColumns';
|
|||||||
import { Cell, EditCell } from './index';
|
import { Cell, EditCell } from './index';
|
||||||
import { TableHead } from './TableHead/TableHead';
|
import { TableHead } from './TableHead/TableHead';
|
||||||
import SettingsPanel from './SettingPanel/SettingPanel';
|
import SettingsPanel from './SettingPanel/SettingPanel';
|
||||||
import { ColumnSelectionOverlay } from './ColumnSelectionOverlay/ColumnSelectionOverlay';
|
|
||||||
|
|
||||||
import { useColumnSettings } from './hooks/useColumnSettings';
|
import { useColumnSettings } from './hooks/useColumnSettings';
|
||||||
import { useTableScale } from './hooks/useTableScale';
|
import { useTableScale } from './hooks/useTableScale';
|
||||||
import { useHeaderPortal } from './hooks/useHeaderPortal';
|
import { useHeaderPortal } from './hooks/useHeaderPortal';
|
||||||
import {
|
import {
|
||||||
BASE_TABLE_CONFIG,
|
BASE_TABLE_CONFIG,
|
||||||
getTableBodyCellProps,
|
getTableHeadCellStyles,
|
||||||
getTablePaperStyles,
|
getTablePaperStyles,
|
||||||
TABLE_ROW_HEIGHT,
|
TABLE_ROW_HEIGHT,
|
||||||
} from './constants/tableConfig';
|
} from './constants/tableConfig';
|
||||||
@ -25,15 +25,14 @@ import { useRealtime } from './contexts/RealtimeContext';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { CircularProgress } from '@mui/material';
|
import { CircularProgress } from '@mui/material';
|
||||||
import { additionVspRowTable } from './constants/addingRowConfig';
|
import { additionVspRowTable } from './constants/addingRowConfig';
|
||||||
import { SelectVspModal } from './Modals/SelectVspModal';
|
import { SelectVspModal } from './Modals/selectVspModal';
|
||||||
import { getRowId } from './utils/rowUtils';
|
import { getRowId } from './utils/rowUtils';
|
||||||
import { debounce } from '@mui/material';
|
|
||||||
|
|
||||||
const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
||||||
const { data, setData, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year);
|
const { data, setData, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction);
|
||||||
const [globalFilter, setGlobalFilter] = useState('');
|
const [globalFilter, setGlobalFilter] = useState('');
|
||||||
const [showColumnFilters, setShowColumnFilters] = useState(false);
|
const [showColumnFilters, setShowColumnFilters] = useState(false);
|
||||||
const [selectedColumnId, setSelectedColumnId] = useState();
|
const [selectedColumn, setSelectedColumn] = useState();
|
||||||
const [rowSelection, setRowSelection] = useState({});
|
const [rowSelection, setRowSelection] = useState({});
|
||||||
const [isLoadingData, setIsLoadingData] = useState(false);
|
const [isLoadingData, setIsLoadingData] = useState(false);
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
@ -47,14 +46,11 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
return additionVspRowTable[formType].includes(sheetName);
|
return additionVspRowTable[formType].includes(sheetName);
|
||||||
}, [formType, sheetName]);
|
}, [formType, sheetName]);
|
||||||
|
|
||||||
const handleGlobalFilterChange = useCallback(
|
const handleGlobalFilterChange = (value) => {
|
||||||
debounce((value) => {
|
startTransition(() => {
|
||||||
startTransition(() => {
|
setGlobalFilter(value);
|
||||||
setGlobalFilter(value);
|
});
|
||||||
});
|
};
|
||||||
}, 300),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isConnected,
|
isConnected,
|
||||||
@ -66,6 +62,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
addRow: contextAddRow,
|
addRow: contextAddRow,
|
||||||
deleteRow: contextDeleteRow,
|
deleteRow: contextDeleteRow,
|
||||||
startEditing: contextStartEditing,
|
startEditing: contextStartEditing,
|
||||||
|
endEditing: contextEndEditing,
|
||||||
} = useRealtime();
|
} = useRealtime();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -92,27 +89,17 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
|
|
||||||
const [columnsConfig, setColumnsConfig] = useState(null);
|
const [columnsConfig, setColumnsConfig] = useState(null);
|
||||||
|
|
||||||
const configCache = useRef(new Map());
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!formType || !sheetName) return;
|
|
||||||
const cacheKey = `${formType}_${sheetName}`;
|
|
||||||
|
|
||||||
if (configCache.current.has(cacheKey)) {
|
|
||||||
setColumnsConfig(configCache.current.get(cacheKey));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadConfig = async () => {
|
const loadConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const { config } = await import(`./constants/${formType}/${sheetName}.js`);
|
const { config } = await import(`./constants/${formType}/${sheetName}.js`);
|
||||||
configCache.current.set(cacheKey, config.config);
|
|
||||||
setColumnsConfig(config.config);
|
setColumnsConfig(config.config);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to load config for ${formType}/${sheetName}:`, error);
|
console.error(`Failed to load config for ${formType}/${sheetName}:`, error);
|
||||||
setColumnsConfig({ columns: [], colors: {} });
|
setColumnsConfig({ columns: [], colors: {} });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadConfig();
|
loadConfig();
|
||||||
}, [formType, sheetName]);
|
}, [formType, sheetName]);
|
||||||
|
|
||||||
@ -126,31 +113,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
}));
|
}));
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleColumnSelect = useCallback((columnId) => {
|
|
||||||
setSelectedColumnId((prev) => {
|
|
||||||
const next = prev === columnId ? undefined : columnId;
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedColumnId) return;
|
|
||||||
|
|
||||||
const handleDocumentClick = (e) => {
|
|
||||||
if (e.target.closest('[data-col-select-id]')) return;
|
|
||||||
if (e.target.closest('[data-pin-panel]')) return;
|
|
||||||
|
|
||||||
setSelectedColumnId(undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleDocumentClick);
|
|
||||||
return () => document.removeEventListener('mousedown', handleDocumentClick);
|
|
||||||
}, [selectedColumnId]);
|
|
||||||
|
|
||||||
const handleCellUpdateError = useCallback((rowId, columnId, error) => {
|
|
||||||
console.error(`Error updating cell ${rowId}_${columnId}:`, error);
|
|
||||||
toast.error('Ошибка обновления ячейки');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
if (!columnsConfig || !columnsConfig.columns) return [];
|
if (!columnsConfig || !columnsConfig.columns) return [];
|
||||||
@ -161,192 +123,164 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
columnsConfig,
|
columnsConfig,
|
||||||
onCellUpdate: handleUpdateCell,
|
onCellUpdate: handleUpdateCell,
|
||||||
onCellNumberClick: handleClickRowCell,
|
onCellNumberClick: handleClickRowCell,
|
||||||
onCellUpdateError: handleCellUpdateError,
|
onCellUpdateError: (rowId, columnId, error) => {
|
||||||
|
console.error(`Error updating cell ${rowId}_${columnId}:`, error);
|
||||||
|
toast.error('Ошибка обновления ячейки')
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating columns:', error);
|
console.error('Error creating columns:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}, [columnsConfig]);
|
}, [columnsConfig, handleUpdateCell]);
|
||||||
|
|
||||||
const selectedColumnIdRef = useRef(selectedColumnId);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
selectedColumnIdRef.current = selectedColumnId;
|
|
||||||
}, [selectedColumnId]);
|
|
||||||
|
|
||||||
const tableMeta = useMemo(() => ({
|
const tableMeta = useMemo(() => ({
|
||||||
updateCell: handleUpdateCell,
|
updateCell: handleUpdateCell,
|
||||||
getSelectedColumnId: () => selectedColumnIdRef.current,
|
|
||||||
}), [handleUpdateCell]);
|
}), [handleUpdateCell]);
|
||||||
|
|
||||||
const ROW_VIRTUALIZER_OPTIONS = {
|
// Конфигурация таблицы
|
||||||
overscan: 5,
|
const tableConfig = useMemo(
|
||||||
scrollPaddingStart: 0,
|
() => ({
|
||||||
scrollPaddingEnd: 0,
|
...BASE_TABLE_CONFIG,
|
||||||
estimateSize: () => TABLE_ROW_HEIGHT,
|
columns,
|
||||||
measureElement: (el) => el?.offsetHeight || TABLE_ROW_HEIGHT,
|
data: data,
|
||||||
};
|
enableRowVirtualization: true,
|
||||||
|
enableColumnVirtualization: true,
|
||||||
const createTableConfig = ({
|
rowVirtualizerInstanceRef: rowVirtualizerRef,
|
||||||
columns,
|
rowVirtualizerOptions: {
|
||||||
data,
|
overscan: 30,
|
||||||
columnSizing,
|
scrollPaddingStart: 0,
|
||||||
columnPinning,
|
scrollPaddingEnd: 0,
|
||||||
columnVisibility,
|
estimateSize: () => TABLE_ROW_HEIGHT,
|
||||||
globalFilter,
|
measureElement: (el) => el?.offsetHeight || TABLE_ROW_HEIGHT,
|
||||||
showColumnFilters,
|
},
|
||||||
rowSelection,
|
onEditingCellChange: (cell) => {
|
||||||
containerRef,
|
if (cell) {
|
||||||
tableMeta,
|
if (editingCell) return;
|
||||||
setColumnSizing,
|
contextStartEditing?.(cell.row, cell.column);
|
||||||
setColumnPinning,
|
} else {
|
||||||
editingCell,
|
contextEndEditing?.(editingCell.row, editingCell.column);
|
||||||
contextStartEditing,
|
|
||||||
}) => ({
|
|
||||||
...BASE_TABLE_CONFIG,
|
|
||||||
columns,
|
|
||||||
data,
|
|
||||||
enableRowVirtualization: true,
|
|
||||||
enableColumnVirtualization: true,
|
|
||||||
rowVirtualizerInstanceRef: rowVirtualizerRef,
|
|
||||||
rowVirtualizerOptions: ROW_VIRTUALIZER_OPTIONS,
|
|
||||||
onEditingCellChange: (cell) => {
|
|
||||||
if (cell && !editingCell) {
|
|
||||||
contextStartEditing?.(cell.row, cell.column);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
columnVirtualizerOptions: ({ table }) => ({
|
|
||||||
overscan: 10,
|
|
||||||
measureElement: (el) => {
|
|
||||||
if (!el) return 150;
|
|
||||||
const index = Number(el?.getAttribute?.('data-index'));
|
|
||||||
const isPinned = Boolean(el?.getAttribute?.('data-pinned'));
|
|
||||||
if (isPinned) {
|
|
||||||
const colId = table.getState().columnPinning.left[index];
|
|
||||||
const column = table.getColumn(colId);
|
|
||||||
return column?.getSize() ?? 150;
|
|
||||||
}
|
}
|
||||||
const allCols = [...table.getLeftVisibleLeafColumns(), ...table.getCenterVisibleLeafColumns()]
|
},
|
||||||
return allCols[index]?.getSize() ?? 150;
|
columnVirtualizerOptions: ({ table }) => ({
|
||||||
|
overscan: 10,
|
||||||
|
measureElement: (el) => {
|
||||||
|
if (!el) return 150;
|
||||||
|
const index = Number(el?.getAttribute?.('data-index'));
|
||||||
|
const isPinned = Boolean(el?.getAttribute?.('data-pinned'));
|
||||||
|
if (isPinned) {
|
||||||
|
const colId = table.getState().columnPinning.left[index];
|
||||||
|
const column = table.getColumn(colId);
|
||||||
|
return column?.getSize() ?? 150;
|
||||||
|
}
|
||||||
|
const allCols = [...table.getLeftVisibleLeafColumns(), ...table.getCenterVisibleLeafColumns()]
|
||||||
|
return allCols[index]?.getSize() ?? 150;
|
||||||
|
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
enableRowSelection: true,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
onColumnSizingChange: setColumnSizing,
|
||||||
|
onColumnPinningChange: setColumnPinning,
|
||||||
|
onGlobalFilterChange: handleGlobalFilterChange,
|
||||||
|
state: {
|
||||||
|
columnSizing,
|
||||||
|
columnPinning,
|
||||||
|
columnVisibility,
|
||||||
|
globalFilter,
|
||||||
|
showColumnFilters,
|
||||||
|
rowSelection,
|
||||||
|
editingCell,
|
||||||
|
},
|
||||||
|
initialState: {
|
||||||
|
expanded: true,
|
||||||
|
},
|
||||||
|
meta: tableMeta,
|
||||||
|
muiTableHeadCellProps: {
|
||||||
|
sx: { boxSizing: 'border-box' },
|
||||||
|
},
|
||||||
|
muiTableBodyCellProps: getTableHeadCellStyles,
|
||||||
|
muiTableHeadProps: {
|
||||||
|
sx: {
|
||||||
|
display: 'table-header-group',
|
||||||
|
height: '1px',
|
||||||
|
minHeight: '1px',
|
||||||
|
maxHeight: '1px',
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
muiTablePaperProps: getTablePaperStyles(),
|
||||||
|
muiTableContainerProps: {
|
||||||
|
ref: containerRef,
|
||||||
|
sx: {
|
||||||
|
position: 'relative',
|
||||||
|
contain: 'layout',
|
||||||
|
minHeight: '100%',
|
||||||
|
'& .MuiTable-root': { position: 'relative' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
muiTableHeadCellFilterTextFieldProps: {
|
||||||
|
placeholder: 'Поиск...',
|
||||||
|
size: 'small',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
enableRowSelection: true,
|
[
|
||||||
onRowSelectionChange: setRowSelection,
|
columns,
|
||||||
onColumnSizingChange: setColumnSizing,
|
data,
|
||||||
onColumnPinningChange: setColumnPinning,
|
setColumnSizing,
|
||||||
onGlobalFilterChange: handleGlobalFilterChange,
|
setColumnPinning,
|
||||||
state: {
|
|
||||||
columnSizing,
|
columnSizing,
|
||||||
columnPinning,
|
columnPinning,
|
||||||
columnVisibility,
|
columnVisibility,
|
||||||
globalFilter,
|
globalFilter,
|
||||||
showColumnFilters,
|
showColumnFilters,
|
||||||
|
containerRef,
|
||||||
|
tableMeta,
|
||||||
rowSelection,
|
rowSelection,
|
||||||
editingCell,
|
editingCell
|
||||||
},
|
],
|
||||||
initialState: {
|
);
|
||||||
expanded: true,
|
|
||||||
},
|
|
||||||
meta: tableMeta,
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: { boxSizing: 'border-box' },
|
|
||||||
},
|
|
||||||
muiTableBodyCellProps: getTableBodyCellProps,
|
|
||||||
muiTableHeadProps: {
|
|
||||||
sx: {
|
|
||||||
display: 'table-header-group',
|
|
||||||
height: '1px',
|
|
||||||
minHeight: '1px',
|
|
||||||
maxHeight: '1px',
|
|
||||||
visibility: 'hidden',
|
|
||||||
'& svg': {
|
|
||||||
height: '1px',
|
|
||||||
minHeight: '1px',
|
|
||||||
maxHeight: '1px',
|
|
||||||
visibility: 'hidden',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
muiTablePaperProps: getTablePaperStyles(),
|
|
||||||
muiTableContainerProps: {
|
|
||||||
ref: containerRef,
|
|
||||||
sx: {
|
|
||||||
position: 'relative',
|
|
||||||
contain: 'layout',
|
|
||||||
minHeight: '100%',
|
|
||||||
'& .MuiTable-root': { position: 'relative' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
muiTableHeadCellFilterTextFieldProps: {
|
|
||||||
placeholder: 'Поиск...',
|
|
||||||
size: 'small',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Конфигурация таблицы
|
|
||||||
const tableConfig = useMemo(() => createTableConfig({
|
|
||||||
columns,
|
|
||||||
data,
|
|
||||||
columnSizing,
|
|
||||||
columnPinning,
|
|
||||||
columnVisibility,
|
|
||||||
globalFilter,
|
|
||||||
showColumnFilters,
|
|
||||||
rowSelection,
|
|
||||||
containerRef,
|
|
||||||
tableMeta,
|
|
||||||
setColumnSizing,
|
|
||||||
setColumnPinning,
|
|
||||||
editingCell,
|
|
||||||
contextStartEditing,
|
|
||||||
}), [
|
|
||||||
columns,
|
|
||||||
data,
|
|
||||||
columnSizing,
|
|
||||||
columnPinning,
|
|
||||||
columnVisibility,
|
|
||||||
globalFilter,
|
|
||||||
showColumnFilters,
|
|
||||||
rowSelection,
|
|
||||||
containerRef,
|
|
||||||
tableMeta,
|
|
||||||
setColumnSizing,
|
|
||||||
setColumnPinning,
|
|
||||||
editingCell,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const table = useMaterialReactTable(tableConfig);
|
const table = useMaterialReactTable(tableConfig);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dataEditingCells?.line_id) {
|
if (!dataEditingCells || !dataEditingCells.line_id) {
|
||||||
setEditingCell(null);
|
setEditingCell(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (editingCell) return;
|
if (editingCell) return;
|
||||||
|
const rowId = dataEditingCells.line_id;
|
||||||
// Отложить поиск до следующего тика
|
const columnId = dataEditingCells.column;
|
||||||
requestAnimationFrame(() => {
|
try {
|
||||||
try {
|
const row = table.getRow(rowId);
|
||||||
const row = table.getRow(dataEditingCells.line_id);
|
const column = table.getColumn(columnId);
|
||||||
const cell = row?.getVisibleCells().find(
|
const cell = row?.getVisibleCells().find(c => c.column.id === columnId);
|
||||||
c => c.column.id === dataEditingCells.column
|
if (cell) {
|
||||||
);
|
setEditingCell(cell);
|
||||||
if (cell) setEditingCell(cell);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
}
|
||||||
});
|
} catch (error) {
|
||||||
}, [dataEditingCells, editingCell, table]);
|
console.error(error);
|
||||||
|
toast.error('Ошибка редактирования ячейки');
|
||||||
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
}
|
||||||
|
}, [dataEditingCells, editingCell]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isFirstLoad && table && data && columns.length !== 0) {
|
if (table && data && columns.length !== 0) {
|
||||||
setIsFirstLoad(false);
|
setIsLoadingData(true)
|
||||||
setIsLoadingData(true);
|
|
||||||
}
|
}
|
||||||
}, [data, columns, table, isFirstLoad]);
|
}, [data, columns, table])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Принудительно обновляем состояние закрепленных колонок
|
||||||
|
// если так не сделать при скролле будут пропадать закрепленные колонки
|
||||||
|
if (columnPinning && Object.keys(columnPinning).length > 0) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setColumnPinning(prev => ({ ...prev }));
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleAddRow = useCallback(() => {
|
const handleAddRow = useCallback(() => {
|
||||||
const allRows = table.getRowModel().rows;
|
const allRows = table.getRowModel().rows;
|
||||||
@ -383,22 +317,10 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
}, [rowSelection, table])
|
}, [rowSelection, table])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
setData([]);
|
|
||||||
setRowSelection({});
|
|
||||||
setEditingCell(null);
|
|
||||||
if (rowVirtualizerRef.current) {
|
|
||||||
rowVirtualizerRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsPanel
|
<SettingsPanel
|
||||||
selectedColumnId={selectedColumnId}
|
selectedColumn={selectedColumn}
|
||||||
columnPinning={columnPinning}
|
|
||||||
table={table}
|
table={table}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
onPinColumn={handlePinColumn}
|
onPinColumn={handlePinColumn}
|
||||||
@ -415,8 +337,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
formId={formId}
|
formId={formId}
|
||||||
sheetName={sheetName}
|
sheetName={sheetName}
|
||||||
direction={direction}
|
direction={direction}
|
||||||
year={year}
|
|
||||||
isProject={formType == 'PROJECT'}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style={tableWrapperStyle}>
|
<div style={tableWrapperStyle}>
|
||||||
@ -432,15 +352,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
>
|
>
|
||||||
<MaterialReactTable table={table} />
|
<MaterialReactTable table={table} />
|
||||||
|
|
||||||
<ColumnSelectionOverlay
|
|
||||||
selectedColumnId={selectedColumnId}
|
|
||||||
containerRef={containerRef}
|
|
||||||
scale={sizeMult}
|
|
||||||
columnSizing={columnSizing}
|
|
||||||
columnPinning={columnPinning}
|
|
||||||
showColumnFilters={showColumnFilters}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isTableLoading && (
|
{isTableLoading && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -462,8 +373,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
<TableHead
|
<TableHead
|
||||||
table={table}
|
table={table}
|
||||||
style={{ zIndex: (theme) => theme.zIndex.modal - 1 }}
|
style={{ zIndex: (theme) => theme.zIndex.modal - 1 }}
|
||||||
selectedColumnId={selectedColumnId}
|
onHeaderSelect={setSelectedColumn}
|
||||||
onColumnSelect={handleColumnSelect}
|
selectHeader={selectedColumn}
|
||||||
onChangeWidth={handleSetColumnWidth}
|
onChangeWidth={handleSetColumnWidth}
|
||||||
isLoadingData={isLoadingData}
|
isLoadingData={isLoadingData}
|
||||||
/>,
|
/>,
|
||||||
@ -477,6 +388,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
onClose={() => setIsOpenModalSelectVsp(false)}
|
onClose={() => setIsOpenModalSelectVsp(false)}
|
||||||
formId={formId}
|
formId={formId}
|
||||||
onSelect={handleAddVspRow}
|
onSelect={handleAddVspRow}
|
||||||
|
// vspOptions={}
|
||||||
|
// selectedVSP={}
|
||||||
title="Выбор ВСП"
|
title="Выбор ВСП"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -18,11 +18,10 @@ import CollapsibleTree from '../../common/CollapsibleTree';
|
|||||||
import ZoomSlider from './ZoomSlider';
|
import ZoomSlider from './ZoomSlider';
|
||||||
import SearchComponent from '../../common/SearchComponent';
|
import SearchComponent from '../../common/SearchComponent';
|
||||||
import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions';
|
import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions';
|
||||||
import { exportSheet, exportSheetProject } from '../../../utils/exportFile';
|
import { exportSheet } from '../../../utils/exportForm';
|
||||||
|
|
||||||
const SettingPanel = ({
|
const SettingPanel = ({
|
||||||
selectedColumnId,
|
selectedColumn,
|
||||||
columnPinning,
|
|
||||||
table,
|
table,
|
||||||
columns,
|
columns,
|
||||||
onPinColumn,
|
onPinColumn,
|
||||||
@ -39,24 +38,18 @@ const SettingPanel = ({
|
|||||||
formId,
|
formId,
|
||||||
sheetName,
|
sheetName,
|
||||||
direction,
|
direction,
|
||||||
year,
|
|
||||||
isProject,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [isPinned, setIsPinned] = useState(false);
|
const [isPinned, setIsPinned] = useState(false);
|
||||||
const [isExporting, setIsExporting] = useState(false);
|
const [isExporting, setIsExporting] = useState(false);
|
||||||
const [selectedColumnIds, setSelectedColumnIds] = useState();
|
const [selectedColumnIds, setSelectedColumnIds] = useState();
|
||||||
const [showColumnFilters, setShowColumnFilters] = useState(false);
|
const [showColumnFilters, setShowColumnFilters] = useState(false);
|
||||||
|
|
||||||
// TODO: console.log(!!!) везде посомтреть
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedColumnId) {
|
if (!selectedColumn) return;
|
||||||
setIsPinned(false);
|
const pinningColumn = table.getState().columnPinning.left;
|
||||||
return;
|
const isPinned = pinningColumn.includes(selectedColumn.id);
|
||||||
}
|
setIsPinned(isPinned);
|
||||||
const pinningColumn = columnPinning?.left || table.getState().columnPinning.left || [];
|
}, [selectedColumn, table]);
|
||||||
const pinned = pinningColumn.includes(selectedColumnId);
|
|
||||||
setIsPinned(pinned);
|
|
||||||
}, [selectedColumnId, columnPinning, table]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedColumnIds(
|
setSelectedColumnIds(
|
||||||
@ -78,16 +71,7 @@ const SettingPanel = ({
|
|||||||
const handleClickDeleteFormula = () => { };
|
const handleClickDeleteFormula = () => { };
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
if (isExporting) return;
|
if (!formId || !sheetName || isExporting) return;
|
||||||
if (isProject) {
|
|
||||||
if (!formId || !sheetName || !year) return;
|
|
||||||
setIsExporting(true);
|
|
||||||
await exportSheetProject(formId, sheetName, year);
|
|
||||||
setIsExporting(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formId || !sheetName) return;
|
|
||||||
setIsExporting(true);
|
setIsExporting(true);
|
||||||
await exportSheet(formId, sheetName, direction);
|
await exportSheet(formId, sheetName, direction);
|
||||||
setIsExporting(false);
|
setIsExporting(false);
|
||||||
@ -95,7 +79,7 @@ const SettingPanel = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Panel data-pin-panel>
|
<Panel>
|
||||||
<Stack direction="row" sx={{ gap: '1.25rem' }}>
|
<Stack direction="row" sx={{ gap: '1.25rem' }}>
|
||||||
<GroupByObject title="Отображение">
|
<GroupByObject title="Отображение">
|
||||||
{/* <ColorPickerButton onColorApply={addColorForCells} /> */}
|
{/* <ColorPickerButton onColorApply={addColorForCells} /> */}
|
||||||
@ -104,48 +88,51 @@ const SettingPanel = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
data-active={isPinned}
|
data-active={isPinned}
|
||||||
disabled={!selectedColumnId}
|
onClick={() =>
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
|
||||||
onClick={() => {
|
|
||||||
if (!selectedColumnId) {
|
|
||||||
console.warn('[ColumnPin] клик Pin: selectedColumnId пустой');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log('[ColumnPin] клик Pin:', {
|
|
||||||
selectedColumnId,
|
|
||||||
isPinned,
|
|
||||||
action: isPinned ? 'unpin' : 'pin',
|
|
||||||
});
|
|
||||||
isPinned
|
isPinned
|
||||||
? onUnpinColumn?.(selectedColumnId)
|
? onUnpinColumn?.(selectedColumn.id)
|
||||||
: onPinColumn?.(selectedColumnId);
|
: onPinColumn?.(selectedColumn.id)
|
||||||
}}
|
}
|
||||||
>
|
>
|
||||||
<Pin />
|
<Pin />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</GroupByObject>
|
</GroupByObject>
|
||||||
|
|
||||||
{!isProject &&
|
<Divider orientation="vertical" flexItem />
|
||||||
<>
|
|
||||||
<Divider orientation="vertical" flexItem />
|
|
||||||
<GroupByObject title="Строки">
|
|
||||||
<Tooltip title="Добавить строку">
|
|
||||||
<IconButton onClick={onAddRow} variant="outlined">
|
|
||||||
<Plus />
|
|
||||||
<AddLine />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip title="Удалить строку">
|
<GroupByObject title="Строки">
|
||||||
<IconButton onClick={onDeleteRow} variant="outlined">
|
<Tooltip title="Добавить строку">
|
||||||
<Minus />
|
<IconButton onClick={onAddRow} variant="outlined">
|
||||||
<RemoveLine />
|
<Plus />
|
||||||
</IconButton>
|
<AddLine />
|
||||||
</Tooltip>
|
</IconButton>
|
||||||
</GroupByObject>
|
</Tooltip>
|
||||||
</>
|
|
||||||
}
|
<Tooltip title="Удалить строку">
|
||||||
|
<IconButton onClick={onDeleteRow} variant="outlined">
|
||||||
|
<Minus />
|
||||||
|
<RemoveLine />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</GroupByObject>
|
||||||
|
|
||||||
|
{/* <Divider orientation="vertical" flexItem />
|
||||||
|
|
||||||
|
<GroupByObject title="Формулы">
|
||||||
|
<Tooltip title="Добавить формулу">
|
||||||
|
<IconButton onClick={handleClickAddFormula} variant="outlined">
|
||||||
|
<FormulaSettingPanel />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip title="Удалить формулу">
|
||||||
|
<IconButton onClick={handleClickDeleteFormula} variant="outlined">
|
||||||
|
<Minus />
|
||||||
|
<FormulaSettingPanel />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</GroupByObject> */}
|
||||||
<Divider orientation="vertical" flexItem />
|
<Divider orientation="vertical" flexItem />
|
||||||
<GroupByObject title="Столбцы">
|
<GroupByObject title="Столбцы">
|
||||||
{columns ? (
|
{columns ? (
|
||||||
|
|||||||
@ -2,12 +2,9 @@ import React from 'react';
|
|||||||
import ColumnResizer from './ColumnResizer';
|
import ColumnResizer from './ColumnResizer';
|
||||||
import { columnPinningDefault } from '../constants/columnConfig';
|
import { columnPinningDefault } from '../constants/columnConfig';
|
||||||
|
|
||||||
const getPinnedColumnIds = (table) =>
|
|
||||||
(table.getState().columnPinning.left || []).filter(Boolean);
|
|
||||||
|
|
||||||
const getSortedColumns = (table) => {
|
const getSortedColumns = (table) => {
|
||||||
const allLeafColumns = table.getAllLeafColumns();
|
const allLeafColumns = table.getAllLeafColumns();
|
||||||
const pinnedColumns = getPinnedColumnIds(table);
|
const pinnedColumns = table.getState().columnPinning.left || [];
|
||||||
|
|
||||||
return [...allLeafColumns].sort((a, b) => {
|
return [...allLeafColumns].sort((a, b) => {
|
||||||
const aIsPinned = pinnedColumns.includes(a.id);
|
const aIsPinned = pinnedColumns.includes(a.id);
|
||||||
@ -49,7 +46,7 @@ const getPinnedColumnsWidth = (header, pinningColumn) => {
|
|||||||
|
|
||||||
const calculateLeftOffset = (table, columnOrHeader) => {
|
const calculateLeftOffset = (table, columnOrHeader) => {
|
||||||
const columnId = getColumnId(columnOrHeader);
|
const columnId = getColumnId(columnOrHeader);
|
||||||
const pinnedColumns = getPinnedColumnIds(table);
|
const pinnedColumns = table.getState().columnPinning.left || [];
|
||||||
const currentPinnedIndex = pinnedColumns.indexOf(columnId);
|
const currentPinnedIndex = pinnedColumns.indexOf(columnId);
|
||||||
|
|
||||||
let leftOffset = 0;
|
let leftOffset = 0;
|
||||||
@ -67,13 +64,13 @@ const calculateLeftOffset = (table, columnOrHeader) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isColumnPin = (table, header) => {
|
const isColumnPin = (table, header) => {
|
||||||
const pinningColumn = getPinnedColumnIds(table);
|
const pinningColumn = table.getState().columnPinning.left;
|
||||||
const columnId = getColumnId(header);
|
const columnId = getColumnId(header);
|
||||||
const isPinned = pinningColumn.includes(columnId);
|
const isPinned = pinningColumn.includes(columnId);
|
||||||
return isPinned;
|
return isPinned;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ColumnNumbersRow = ({ table, selectedColumnId, onColumnSelect }) => {
|
const ColumnNumbersRow = ({ table }) => {
|
||||||
const sortedColumns = getSortedColumns(table);
|
const sortedColumns = getSortedColumns(table);
|
||||||
const tableState = table.getState();
|
const tableState = table.getState();
|
||||||
const filteredColumns = sortedColumns.filter(
|
const filteredColumns = sortedColumns.filter(
|
||||||
@ -98,38 +95,27 @@ const ColumnNumbersRow = ({ table, selectedColumnId, onColumnSelect }) => {
|
|||||||
key={`col-number-${column.id}`}
|
key={`col-number-${column.id}`}
|
||||||
column={column}
|
column={column}
|
||||||
table={table}
|
table={table}
|
||||||
selectedColumnId={selectedColumnId}
|
|
||||||
onColumnSelect={onColumnSelect}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ColumnNumberCell = ({ column, table, selectedColumnId, onColumnSelect }) => {
|
const ColumnNumberCell = ({ column, table }) => {
|
||||||
const isPinned = isColumnPin(table, column);
|
const isPinned = isColumnPin(table, column);
|
||||||
const allLeafColumns = table.getAllLeafColumns();
|
const allLeafColumns = table.getAllLeafColumns();
|
||||||
const originalIndex = allLeafColumns.findIndex((col) => col.id === column.id);
|
const originalIndex = allLeafColumns.findIndex((col) => col.id === column.id);
|
||||||
const leftOffset = calculateLeftOffset(table, column);
|
const leftOffset = calculateLeftOffset(table, column);
|
||||||
const selected = selectedColumnId === column.id;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`col-number-${column.id}`}
|
key={`col-number-${column.id}`}
|
||||||
data-col-select-id={column.id}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onColumnSelect?.(column.id);
|
|
||||||
}}
|
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
|
||||||
style={{
|
style={{
|
||||||
width: column.getSize(),
|
width: column.getSize(),
|
||||||
minWidth: column.getSize(),
|
minWidth: column.getSize(),
|
||||||
maxWidth: column.getSize(),
|
maxWidth: column.getSize(),
|
||||||
padding: '2px 6px',
|
padding: '2px 6px',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
backgroundColor: selected ? '#589A2F' : '#FBFBFB',
|
backgroundColor: '#e5e5e5',
|
||||||
color: selected ? '#ffffff' : '#5E5E5E',
|
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -139,9 +125,7 @@ const ColumnNumberCell = ({ column, table, selectedColumnId, onColumnSelect }) =
|
|||||||
borderBottom: 'none',
|
borderBottom: 'none',
|
||||||
position: isPinned ? 'sticky' : 'relative',
|
position: isPinned ? 'sticky' : 'relative',
|
||||||
left: isPinned ? leftOffset : 'auto',
|
left: isPinned ? leftOffset : 'auto',
|
||||||
zIndex: selected ? 16 : isPinned ? 20 : 'auto',
|
zIndex: isPinned ? 20 : 'auto',
|
||||||
cursor: 'pointer',
|
|
||||||
userSelect: 'none',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{originalIndex}
|
{originalIndex}
|
||||||
@ -149,9 +133,9 @@ const ColumnNumberCell = ({ column, table, selectedColumnId, onColumnSelect }) =
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
|
const HeaderCell = ({ header, table, onClick, hightlight, onChangeWidth }) => {
|
||||||
const column = header.column;
|
const column = header.column;
|
||||||
const pinningColumn = getPinnedColumnIds(table);
|
const pinningColumn = table.getState().columnPinning.left || [];
|
||||||
const isGroup = header.subHeaders?.length > 0;
|
const isGroup = header.subHeaders?.length > 0;
|
||||||
const isPinned = isColumnPin(table, header);
|
const isPinned = isColumnPin(table, header);
|
||||||
const leftOffset = calculateLeftOffset(table, header);
|
const leftOffset = calculateLeftOffset(table, header);
|
||||||
@ -164,7 +148,7 @@ const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
|
|||||||
|
|
||||||
const backgroundColor = customBgColor
|
const backgroundColor = customBgColor
|
||||||
? customBgColor
|
? customBgColor
|
||||||
: 'rgba(243, 244, 246, 1)';
|
: (hightlight ? '#cbccce' : 'rgba(243, 244, 246, 1)');
|
||||||
|
|
||||||
const handleChangeWidth = (deltaWidth) => {
|
const handleChangeWidth = (deltaWidth) => {
|
||||||
const size = header.getSize();
|
const size = header.getSize();
|
||||||
@ -180,7 +164,7 @@ const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
|
|||||||
}
|
}
|
||||||
// Для незакрепленных колонок текст должен начинаться после всех закрепленных
|
// Для незакрепленных колонок текст должен начинаться после всех закрепленных
|
||||||
// Получаем общую ширину всех закрепленных колонок
|
// Получаем общую ширину всех закрепленных колонок
|
||||||
const pinnedColumns = getPinnedColumnIds(table);
|
const pinnedColumns = table.getState().columnPinning.left || [];
|
||||||
let totalPinnedWidth = 0;
|
let totalPinnedWidth = 0;
|
||||||
for (const pinnedId of pinnedColumns) {
|
for (const pinnedId of pinnedColumns) {
|
||||||
const pinnedCol = table.getColumn(pinnedId);
|
const pinnedCol = table.getColumn(pinnedId);
|
||||||
@ -204,7 +188,6 @@ const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
lineHeight: 1,
|
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
color: '#424242',
|
color: '#424242',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
@ -313,10 +296,10 @@ const FilterCell = ({ column, table }) => {
|
|||||||
onChange={(e) => handleFilterChange(e.target.value)}
|
onChange={(e) => handleFilterChange(e.target.value)}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '0.25rem 0.375rem',
|
padding: '4px 6px',
|
||||||
fontSize: '0.6875rem',
|
fontSize: '11px',
|
||||||
border: '1px solid #ddd',
|
border: '1px solid #ddd',
|
||||||
borderRadius: '0.25rem',
|
borderRadius: '4px',
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -330,30 +313,23 @@ const FilterCell = ({ column, table }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
case 'range': {
|
case 'range': {
|
||||||
|
// Для диапазона можно реализовать два поля
|
||||||
const [min, max] = column.getFilterValue() || ['', ''];
|
const [min, max] = column.getFilterValue() || ['', ''];
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', gap: '0.25rem' }}> {/* 4px */}
|
<div style={{ display: 'flex', gap: '4px' }}>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="От"
|
placeholder="От"
|
||||||
value={min}
|
value={min}
|
||||||
onChange={(e) => column.setFilterValue([e.target.value, max])}
|
onChange={(e) => column.setFilterValue([e.target.value, max])}
|
||||||
style={{
|
style={{ width: '50%', padding: '4px', fontSize: '11px' }}
|
||||||
width: '50%',
|
|
||||||
padding: '0.25rem',
|
|
||||||
fontSize: '0.6875rem'
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="До"
|
placeholder="До"
|
||||||
value={max}
|
value={max}
|
||||||
onChange={(e) => column.setFilterValue([min, e.target.value])}
|
onChange={(e) => column.setFilterValue([min, e.target.value])}
|
||||||
style={{
|
style={{ width: '50%', padding: '4px', fontSize: '11px' }}
|
||||||
width: '50%',
|
|
||||||
padding: '0.25rem',
|
|
||||||
fontSize: '0.6875rem'
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -368,10 +344,10 @@ const FilterCell = ({ column, table }) => {
|
|||||||
placeholder="Поиск..."
|
placeholder="Поиск..."
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '0.25rem 0.375rem',
|
padding: '4px 6px',
|
||||||
fontSize: '0.6875rem',
|
fontSize: '11px',
|
||||||
border: '1px solid #ddd',
|
border: '1px solid #ddd',
|
||||||
borderRadius: '0.25rem',
|
borderRadius: '4px',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -403,15 +379,13 @@ const FilterCell = ({ column, table }) => {
|
|||||||
|
|
||||||
export const TableHead = ({
|
export const TableHead = ({
|
||||||
table,
|
table,
|
||||||
selectedColumnId,
|
onHeaderSelect,
|
||||||
onColumnSelect,
|
selectHeader,
|
||||||
onChangeWidth,
|
onChangeWidth,
|
||||||
isLoadingData,
|
isLoadingData,
|
||||||
}) => {
|
}) => {
|
||||||
const handleHeaderClick = (header) => {
|
const handleHeaderClick = (header) => {
|
||||||
if (!header.subHeaders?.length && header.column?.id) {
|
onHeaderSelect(header);
|
||||||
onColumnSelect?.(header.column.id);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isLoadingData) {
|
if (!isLoadingData) {
|
||||||
@ -428,15 +402,9 @@ export const TableHead = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Строка с номерами колонок */}
|
{/* Строка с номерами колонок */}
|
||||||
<ColumnNumbersRow
|
<ColumnNumbersRow table={table} />
|
||||||
table={table}
|
|
||||||
selectedColumnId={selectedColumnId}
|
|
||||||
onColumnSelect={onColumnSelect}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{table.getState().showColumnFilters && (
|
{table.getState().showColumnFilters && <FilterRow table={table} />}
|
||||||
<FilterRow table={table} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Существующие строки заголовков */}
|
{/* Существующие строки заголовков */}
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
@ -445,6 +413,9 @@ export const TableHead = ({
|
|||||||
<React.Fragment key={header.id}>
|
<React.Fragment key={header.id}>
|
||||||
<HeaderCell
|
<HeaderCell
|
||||||
header={header}
|
header={header}
|
||||||
|
hightlight={
|
||||||
|
selectHeader ? header.id == selectHeader.id : false
|
||||||
|
}
|
||||||
table={table}
|
table={table}
|
||||||
onClick={handleHeaderClick}
|
onClick={handleHeaderClick}
|
||||||
onChangeWidth={onChangeWidth}
|
onChangeWidth={onChangeWidth}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
555
web/src/components/RealtimeTable/constants/FORM_3/LP.js
Normal file
555
web/src/components/RealtimeTable/constants/FORM_3/LP.js
Normal file
@ -0,0 +1,555 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
greenColumn,
|
||||||
|
whiteColumn,
|
||||||
|
orangeColumn,
|
||||||
|
yellowColumn,
|
||||||
|
redColumn,
|
||||||
|
blueColumn,
|
||||||
|
} from '../columnColors';
|
||||||
|
export const config = {
|
||||||
|
"config":{
|
||||||
|
"colors":{
|
||||||
|
"kod_razdela_kod_razdela":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"kod_razdela_kod_razdela"
|
||||||
|
},
|
||||||
|
"id_stati_id_stati":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"id_stati_id_stati"
|
||||||
|
},
|
||||||
|
"id_gruppy_nomenklatury_id_gruppy_nomenklatury":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"id_gruppy_nomenklatury_id_gruppy_nomenklatury"
|
||||||
|
},
|
||||||
|
"naimenovanie_naimenovanie":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"naimenovanie_naimenovanie"
|
||||||
|
},
|
||||||
|
"korrektirovka_limita_po_statyam_smety":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"korrektirovka_limita_po_statyam_smety"
|
||||||
|
},
|
||||||
|
"field_uvelichenie_smety":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_uvelichenie_smety"
|
||||||
|
},
|
||||||
|
"field_za_yanvar":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_yanvar"
|
||||||
|
},
|
||||||
|
"field_za_fevral":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_fevral"
|
||||||
|
},
|
||||||
|
"field_itogo_i_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_itogo_i_kvartal"
|
||||||
|
},
|
||||||
|
"field_za_aprel":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_aprel"
|
||||||
|
},
|
||||||
|
"field_za_may":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_may"
|
||||||
|
},
|
||||||
|
"field_itogo_ii_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_itogo_ii_kvartal"
|
||||||
|
},
|
||||||
|
"field_za_iyul":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_iyul"
|
||||||
|
},
|
||||||
|
"field_za_avgust":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_avgust"
|
||||||
|
},
|
||||||
|
"field_itogo_iii_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_itogo_iii_kvartal"
|
||||||
|
},
|
||||||
|
"field_za_oktyabr":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_oktyabr"
|
||||||
|
},
|
||||||
|
"field_za_noyabr":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_noyabr"
|
||||||
|
},
|
||||||
|
"field_spod":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_spod"
|
||||||
|
},
|
||||||
|
"field_itogo_iv_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_itogo_iv_kvartal"
|
||||||
|
},
|
||||||
|
"itogo_itogo":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"itogo_itogo"
|
||||||
|
},
|
||||||
|
"fakt_zamart":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"fakt_zamart"
|
||||||
|
},
|
||||||
|
"fakt_zaiyun":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"fakt_zaiyun"
|
||||||
|
},
|
||||||
|
"fakt_zasentyabr":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"fakt_zasentyabr"
|
||||||
|
},
|
||||||
|
"fakt_zadekabr":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"fakt_zadekabr"
|
||||||
|
},
|
||||||
|
"ekonomiya_pereraskhod_ekonomiya_pereraskhod":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"ekonomiya_pereraskhod_ekonomiya_pereraskhod"
|
||||||
|
},
|
||||||
|
"limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal"
|
||||||
|
},
|
||||||
|
"limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal"
|
||||||
|
},
|
||||||
|
"limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Код раздела",
|
||||||
|
"accessorKey":"kod_razdela",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Код раздела",
|
||||||
|
"accessorKey":"kod_razdela_kod_razdela",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"ID статьи",
|
||||||
|
"accessorKey":"id_stati",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"ID статьи",
|
||||||
|
"accessorKey":"id_stati_id_stati",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"ID группы номенклатуры",
|
||||||
|
"accessorKey":"id_gruppy_nomenklatury",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"ID группы номенклатуры",
|
||||||
|
"accessorKey":"id_gruppy_nomenklatury_id_gruppy_nomenklatury",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Наименование",
|
||||||
|
"accessorKey":"naimenovanie",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Наименование",
|
||||||
|
"accessorKey":"naimenovanie_naimenovanie",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Корректировка лимита",
|
||||||
|
"accessorKey":"korrektirovka_limita",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"по статьям сметы *",
|
||||||
|
"accessorKey":"korrektirovka_limita_po_statyam_smety",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"",
|
||||||
|
"accessorKey":"field",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"увеличение сметы **",
|
||||||
|
"accessorKey":"field_uvelichenie_smety",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за ЯНВАРЬ",
|
||||||
|
"accessorKey":"field_za_yanvar",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за ФЕВРАЛЬ",
|
||||||
|
"accessorKey":"field_za_fevral",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Итого I квартал",
|
||||||
|
"accessorKey":"field_itogo_i_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за АПРЕЛЬ",
|
||||||
|
"accessorKey":"field_za_aprel",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за МАЙ",
|
||||||
|
"accessorKey":"field_za_may",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Итого II квартал",
|
||||||
|
"accessorKey":"field_itogo_ii_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за ИЮЛЬ",
|
||||||
|
"accessorKey":"field_za_iyul",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за АВГУСТ",
|
||||||
|
"accessorKey":"field_za_avgust",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Итого III квартал",
|
||||||
|
"accessorKey":"field_itogo_iii_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за ОКТЯБРЬ",
|
||||||
|
"accessorKey":"field_za_oktyabr",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за НОЯБРЬ",
|
||||||
|
"accessorKey":"field_za_noyabr",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"СПОД",
|
||||||
|
"accessorKey":"field_spod",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Итого IV квартал",
|
||||||
|
"accessorKey":"field_itogo_iv_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Итого",
|
||||||
|
"accessorKey":"itogo",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Итого",
|
||||||
|
"accessorKey":"itogo_itogo",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Факт",
|
||||||
|
"accessorKey":"fakt",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"заМАРТ",
|
||||||
|
"accessorKey":"fakt_zamart",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"заИЮНЬ",
|
||||||
|
"accessorKey":"fakt_zaiyun",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"заСЕНТЯБРЬ",
|
||||||
|
"accessorKey":"fakt_zasentyabr",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"заДЕКАБРЬ",
|
||||||
|
"accessorKey":"fakt_zadekabr",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"экономия (+) перерасход (-)",
|
||||||
|
"accessorKey":"ekonomiya_pereraskhod",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"экономия (+) перерасход (-)",
|
||||||
|
"accessorKey":"ekonomiya_pereraskhod_ekonomiya_pereraskhod",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Лимит затрат / остаток лимита затрат на II квартал",
|
||||||
|
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Лимит затрат / остаток лимита затрат на II квартал",
|
||||||
|
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Лимит затрат / остаток лимита затрат на III квартал",
|
||||||
|
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Лимит затрат / остаток лимита затрат на III квартал",
|
||||||
|
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Лимит затрат / остаток лимита затрат на IV квартал",
|
||||||
|
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Лимит затрат / остаток лимита затрат на IV квартал",
|
||||||
|
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
577
web/src/components/RealtimeTable/constants/FORM_3/PROJECT.js
Normal file
577
web/src/components/RealtimeTable/constants/FORM_3/PROJECT.js
Normal file
@ -0,0 +1,577 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
greenColumn,
|
||||||
|
whiteColumn,
|
||||||
|
orangeColumn,
|
||||||
|
yellowColumn,
|
||||||
|
redColumn,
|
||||||
|
blueColumn,
|
||||||
|
} from '../columnColors';
|
||||||
|
export const config = {
|
||||||
|
"config":{
|
||||||
|
"colors":{
|
||||||
|
"kod_razdela_kod_razdela":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"kod_razdela_kod_razdela"
|
||||||
|
},
|
||||||
|
"id_stati_id_stati":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"id_stati_id_stati"
|
||||||
|
},
|
||||||
|
"id_gruppy_nomenklatury_id_gruppy_nomenklatury":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"id_gruppy_nomenklatury_id_gruppy_nomenklatury"
|
||||||
|
},
|
||||||
|
"naimenovanie_naimenovanie":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"naimenovanie_naimenovanie"
|
||||||
|
},
|
||||||
|
"korrektirovka_plana_po_statyam_smety":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"korrektirovka_plana_po_statyam_smety"
|
||||||
|
},
|
||||||
|
"field_uvelichenie_smety":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_uvelichenie_smety"
|
||||||
|
},
|
||||||
|
"field_za_yanvar":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_yanvar"
|
||||||
|
},
|
||||||
|
"field_za_fevral":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_fevral"
|
||||||
|
},
|
||||||
|
"field_itogo_i_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_itogo_i_kvartal"
|
||||||
|
},
|
||||||
|
"field_za_aprel":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_aprel"
|
||||||
|
},
|
||||||
|
"field_za_may":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_may"
|
||||||
|
},
|
||||||
|
"field_itogo_ii_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_itogo_ii_kvartal"
|
||||||
|
},
|
||||||
|
"field_za_iyul":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_iyul"
|
||||||
|
},
|
||||||
|
"field_za_avgust":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_avgust"
|
||||||
|
},
|
||||||
|
"field_itogo_iii_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_itogo_iii_kvartal"
|
||||||
|
},
|
||||||
|
"field_za_oktyabr":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_oktyabr"
|
||||||
|
},
|
||||||
|
"field_za_noyabr":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_noyabr"
|
||||||
|
},
|
||||||
|
"field_spod":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_spod"
|
||||||
|
},
|
||||||
|
"field_itogo_iv_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_itogo_iv_kvartal"
|
||||||
|
},
|
||||||
|
"itogo_korrektirovka_itogo_korrektirovka":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"itogo_korrektirovka_itogo_korrektirovka"
|
||||||
|
},
|
||||||
|
"fakt_zamart":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"fakt_zamart"
|
||||||
|
},
|
||||||
|
"fakt_zaiyun":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"fakt_zaiyun"
|
||||||
|
},
|
||||||
|
"fakt_zasentyabr":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"fakt_zasentyabr"
|
||||||
|
},
|
||||||
|
"fakt_zadekabr":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"fakt_zadekabr"
|
||||||
|
},
|
||||||
|
"ekonomiya_pereraskhod_ekonomiya_pereraskhod":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"ekonomiya_pereraskhod_ekonomiya_pereraskhod"
|
||||||
|
},
|
||||||
|
"smeta_raskhodov_po_razvitiyu_na_ii_kvartal_smeta_raskhodov_po_razvitiyu_na_ii_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"smeta_raskhodov_po_razvitiyu_na_ii_kvartal_smeta_raskhodov_po_razvitiyu_na_ii_kvartal"
|
||||||
|
},
|
||||||
|
"itogo_skorrek_smeta_itogo_skorrek_smeta":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"itogo_skorrek_smeta_itogo_skorrek_smeta"
|
||||||
|
},
|
||||||
|
"smeta_raskhodov_po_razvitiyu_na_iii_kvartal_smeta_raskhodov_po_razvitiyu_na_iii_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"smeta_raskhodov_po_razvitiyu_na_iii_kvartal_smeta_raskhodov_po_razvitiyu_na_iii_kvartal"
|
||||||
|
},
|
||||||
|
"smeta_raskhodov_po_razvitiyu_na_iv_kvartal_smeta_raskhodov_po_razvitiyu_na_iv_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"smeta_raskhodov_po_razvitiyu_na_iv_kvartal_smeta_raskhodov_po_razvitiyu_na_iv_kvartal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Код раздела",
|
||||||
|
"accessorKey":"kod_razdela",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Код раздела",
|
||||||
|
"accessorKey":"kod_razdela_kod_razdela",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"ID статьи",
|
||||||
|
"accessorKey":"id_stati",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"ID статьи",
|
||||||
|
"accessorKey":"id_stati_id_stati",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"ID группы номенклатуры",
|
||||||
|
"accessorKey":"id_gruppy_nomenklatury",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"ID группы номенклатуры",
|
||||||
|
"accessorKey":"id_gruppy_nomenklatury_id_gruppy_nomenklatury",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Наименование",
|
||||||
|
"accessorKey":"naimenovanie",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Наименование",
|
||||||
|
"accessorKey":"naimenovanie_naimenovanie",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Корректировка плана",
|
||||||
|
"accessorKey":"korrektirovka_plana",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"по статьям сметы *",
|
||||||
|
"accessorKey":"korrektirovka_plana_po_statyam_smety",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"",
|
||||||
|
"accessorKey":"field",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"увеличение сметы **",
|
||||||
|
"accessorKey":"field_uvelichenie_smety",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за ЯНВАРЬ",
|
||||||
|
"accessorKey":"field_za_yanvar",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за ФЕВРАЛЬ",
|
||||||
|
"accessorKey":"field_za_fevral",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Итого I квартал",
|
||||||
|
"accessorKey":"field_itogo_i_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за АПРЕЛЬ",
|
||||||
|
"accessorKey":"field_za_aprel",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за МАЙ",
|
||||||
|
"accessorKey":"field_za_may",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Итого II квартал",
|
||||||
|
"accessorKey":"field_itogo_ii_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за ИЮЛЬ",
|
||||||
|
"accessorKey":"field_za_iyul",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за АВГУСТ",
|
||||||
|
"accessorKey":"field_za_avgust",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Итого III квартал",
|
||||||
|
"accessorKey":"field_itogo_iii_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за ОКТЯБРЬ",
|
||||||
|
"accessorKey":"field_za_oktyabr",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за НОЯБРЬ",
|
||||||
|
"accessorKey":"field_za_noyabr",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"СПОД",
|
||||||
|
"accessorKey":"field_spod",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Итого IV квартал",
|
||||||
|
"accessorKey":"field_itogo_iv_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Итого корректировка",
|
||||||
|
"accessorKey":"itogo_korrektirovka",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Итого корректировка",
|
||||||
|
"accessorKey":"itogo_korrektirovka_itogo_korrektirovka",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Факт",
|
||||||
|
"accessorKey":"fakt",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"заМАРТ",
|
||||||
|
"accessorKey":"fakt_zamart",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"заИЮНЬ",
|
||||||
|
"accessorKey":"fakt_zaiyun",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"заСЕНТЯБРЬ",
|
||||||
|
"accessorKey":"fakt_zasentyabr",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"заДЕКАБРЬ",
|
||||||
|
"accessorKey":"fakt_zadekabr",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"экономия (+) перерасход (-)",
|
||||||
|
"accessorKey":"ekonomiya_pereraskhod",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"экономия (+) перерасход (-)",
|
||||||
|
"accessorKey":"ekonomiya_pereraskhod_ekonomiya_pereraskhod",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Смета расходов по развитию на II квартал",
|
||||||
|
"accessorKey":"smeta_raskhodov_po_razvitiyu_na_ii_kvartal",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Смета расходов по развитию на II квартал",
|
||||||
|
"accessorKey":"smeta_raskhodov_po_razvitiyu_na_ii_kvartal_smeta_raskhodov_po_razvitiyu_na_ii_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Итого скоррек. смета",
|
||||||
|
"accessorKey":"itogo_skorrek_smeta",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Итого скоррек. смета",
|
||||||
|
"accessorKey":"itogo_skorrek_smeta_itogo_skorrek_smeta",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Смета расходов по развитию на III квартал",
|
||||||
|
"accessorKey":"smeta_raskhodov_po_razvitiyu_na_iii_kvartal",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Смета расходов по развитию на III квартал",
|
||||||
|
"accessorKey":"smeta_raskhodov_po_razvitiyu_na_iii_kvartal_smeta_raskhodov_po_razvitiyu_na_iii_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Смета расходов по развитию на IV квартал",
|
||||||
|
"accessorKey":"smeta_raskhodov_po_razvitiyu_na_iv_kvartal",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Смета расходов по развитию на IV квартал",
|
||||||
|
"accessorKey":"smeta_raskhodov_po_razvitiyu_na_iv_kvartal_smeta_raskhodov_po_razvitiyu_na_iv_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
555
web/src/components/RealtimeTable/constants/FORM_3/TR.js
Normal file
555
web/src/components/RealtimeTable/constants/FORM_3/TR.js
Normal file
@ -0,0 +1,555 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
greenColumn,
|
||||||
|
whiteColumn,
|
||||||
|
orangeColumn,
|
||||||
|
yellowColumn,
|
||||||
|
redColumn,
|
||||||
|
blueColumn,
|
||||||
|
} from '../columnColors';
|
||||||
|
export const config = {
|
||||||
|
"config":{
|
||||||
|
"colors":{
|
||||||
|
"kod_razdela_kod_razdela":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"kod_razdela_kod_razdela"
|
||||||
|
},
|
||||||
|
"id_stati_id_stati":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"id_stati_id_stati"
|
||||||
|
},
|
||||||
|
"id_gruppy_nomenklatury_id_gruppy_nomenklatury":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"id_gruppy_nomenklatury_id_gruppy_nomenklatury"
|
||||||
|
},
|
||||||
|
"naimenovanie_naimenovanie":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"naimenovanie_naimenovanie"
|
||||||
|
},
|
||||||
|
"korrektirovka_limita_po_statyam_smety":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"korrektirovka_limita_po_statyam_smety"
|
||||||
|
},
|
||||||
|
"field_uvelichenie_smety":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_uvelichenie_smety"
|
||||||
|
},
|
||||||
|
"field_za_yanvar":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_yanvar"
|
||||||
|
},
|
||||||
|
"field_za_fevral":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_fevral"
|
||||||
|
},
|
||||||
|
"field_itogo_i_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_itogo_i_kvartal"
|
||||||
|
},
|
||||||
|
"field_za_aprel":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_aprel"
|
||||||
|
},
|
||||||
|
"field_za_may":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_may"
|
||||||
|
},
|
||||||
|
"field_itogo_ii_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_itogo_ii_kvartal"
|
||||||
|
},
|
||||||
|
"field_za_iyul":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_iyul"
|
||||||
|
},
|
||||||
|
"field_za_avgust":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_avgust"
|
||||||
|
},
|
||||||
|
"field_itogo_iii_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_itogo_iii_kvartal"
|
||||||
|
},
|
||||||
|
"field_za_oktyabr":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_oktyabr"
|
||||||
|
},
|
||||||
|
"field_za_noyabr":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_za_noyabr"
|
||||||
|
},
|
||||||
|
"field_spod":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_spod"
|
||||||
|
},
|
||||||
|
"field_itogo_iv_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"field_itogo_iv_kvartal"
|
||||||
|
},
|
||||||
|
"itogo_itogo":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"itogo_itogo"
|
||||||
|
},
|
||||||
|
"fakt_zamart":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"fakt_zamart"
|
||||||
|
},
|
||||||
|
"fakt_zaiyun":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"fakt_zaiyun"
|
||||||
|
},
|
||||||
|
"fakt_zasentyabr":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"fakt_zasentyabr"
|
||||||
|
},
|
||||||
|
"fakt_zadekabr":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"fakt_zadekabr"
|
||||||
|
},
|
||||||
|
"ekonomiya_pereraskhod_ekonomiya_pereraskhod":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"ekonomiya_pereraskhod_ekonomiya_pereraskhod"
|
||||||
|
},
|
||||||
|
"limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal"
|
||||||
|
},
|
||||||
|
"limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal"
|
||||||
|
},
|
||||||
|
"limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal":{
|
||||||
|
"color_type":whiteColumn,
|
||||||
|
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Код раздела",
|
||||||
|
"accessorKey":"kod_razdela",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Код раздела",
|
||||||
|
"accessorKey":"kod_razdela_kod_razdela",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"ID статьи",
|
||||||
|
"accessorKey":"id_stati",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"ID статьи",
|
||||||
|
"accessorKey":"id_stati_id_stati",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"ID группы номенклатуры",
|
||||||
|
"accessorKey":"id_gruppy_nomenklatury",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"ID группы номенклатуры",
|
||||||
|
"accessorKey":"id_gruppy_nomenklatury_id_gruppy_nomenklatury",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Наименование",
|
||||||
|
"accessorKey":"naimenovanie",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Наименование",
|
||||||
|
"accessorKey":"naimenovanie_naimenovanie",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Корректировка лимита",
|
||||||
|
"accessorKey":"korrektirovka_limita",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"по статьям сметы *",
|
||||||
|
"accessorKey":"korrektirovka_limita_po_statyam_smety",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"",
|
||||||
|
"accessorKey":"field",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"увеличение сметы **",
|
||||||
|
"accessorKey":"field_uvelichenie_smety",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за ЯНВАРЬ",
|
||||||
|
"accessorKey":"field_za_yanvar",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за ФЕВРАЛЬ",
|
||||||
|
"accessorKey":"field_za_fevral",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Итого I квартал",
|
||||||
|
"accessorKey":"field_itogo_i_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за АПРЕЛЬ",
|
||||||
|
"accessorKey":"field_za_aprel",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за МАЙ",
|
||||||
|
"accessorKey":"field_za_may",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Итого II квартал",
|
||||||
|
"accessorKey":"field_itogo_ii_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за ИЮЛЬ",
|
||||||
|
"accessorKey":"field_za_iyul",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за АВГУСТ",
|
||||||
|
"accessorKey":"field_za_avgust",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Итого III квартал",
|
||||||
|
"accessorKey":"field_itogo_iii_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за ОКТЯБРЬ",
|
||||||
|
"accessorKey":"field_za_oktyabr",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"за НОЯБРЬ",
|
||||||
|
"accessorKey":"field_za_noyabr",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"СПОД",
|
||||||
|
"accessorKey":"field_spod",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Итого IV квартал",
|
||||||
|
"accessorKey":"field_itogo_iv_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Итого",
|
||||||
|
"accessorKey":"itogo",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Итого",
|
||||||
|
"accessorKey":"itogo_itogo",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Факт",
|
||||||
|
"accessorKey":"fakt",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"заМАРТ",
|
||||||
|
"accessorKey":"fakt_zamart",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"заИЮНЬ",
|
||||||
|
"accessorKey":"fakt_zaiyun",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"заСЕНТЯБРЬ",
|
||||||
|
"accessorKey":"fakt_zasentyabr",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"заДЕКАБРЬ",
|
||||||
|
"accessorKey":"fakt_zadekabr",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains",
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"экономия (+) перерасход (-)",
|
||||||
|
"accessorKey":"ekonomiya_pereraskhod",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"экономия (+) перерасход (-)",
|
||||||
|
"accessorKey":"ekonomiya_pereraskhod_ekonomiya_pereraskhod",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Лимит затрат / остаток лимита затрат на II квартал",
|
||||||
|
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Лимит затрат / остаток лимита затрат на II квартал",
|
||||||
|
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_ii_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Лимит затрат / остаток лимита затрат на III квартал",
|
||||||
|
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Лимит затрат / остаток лимита затрат на III квартал",
|
||||||
|
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iii_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header":"Лимит затрат / остаток лимита затрат на IV квартал",
|
||||||
|
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal",
|
||||||
|
"columns":[
|
||||||
|
{
|
||||||
|
"header":"Лимит затрат / остаток лимита затрат на IV квартал",
|
||||||
|
"accessorKey":"limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal_limit_zatrat_ostatok_limita_zatrat_na_iv_kvartal",
|
||||||
|
"size":150,
|
||||||
|
"filterFn":"contains"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"muiTableHeadCellProps":{
|
||||||
|
"sx":{
|
||||||
|
"backgroundColor":"#C2D69B",
|
||||||
|
"color":"#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,869 +0,0 @@
|
|||||||
import {
|
|
||||||
greenColumn,
|
|
||||||
whiteColumn,
|
|
||||||
orangeColumn,
|
|
||||||
yellowColumn,
|
|
||||||
redColumn,
|
|
||||||
blueColumn,
|
|
||||||
} from "../columnColors";
|
|
||||||
export const config = {
|
|
||||||
config: {
|
|
||||||
colors: {
|
|
||||||
"data.section_code": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.section_code",
|
|
||||||
},
|
|
||||||
"data.name": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.name",
|
|
||||||
},
|
|
||||||
"data.plan.support.q1": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.plan.support.q1",
|
|
||||||
},
|
|
||||||
"data.plan.support.q2": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.plan.support.q2",
|
|
||||||
},
|
|
||||||
"data.plan.support.q3": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.plan.support.q3",
|
|
||||||
},
|
|
||||||
"data.plan.support.q4": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.plan.support.q4",
|
|
||||||
},
|
|
||||||
"data.plan.support.year": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.plan.support.year",
|
|
||||||
},
|
|
||||||
"data.plan.development.q1": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.plan.development.q1",
|
|
||||||
},
|
|
||||||
"data.plan.development.q2": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.plan.development.q2",
|
|
||||||
},
|
|
||||||
"data.plan.development.q3": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.plan.development.q3",
|
|
||||||
},
|
|
||||||
"data.plan.development.q4": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.plan.development.q4",
|
|
||||||
},
|
|
||||||
"data.plan.development.year": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.plan.development.year",
|
|
||||||
},
|
|
||||||
itogo_2026_field_field_field: {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "itogo_2026_field_field_field",
|
|
||||||
},
|
|
||||||
"data.approved.support.q1": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.approved.support.q1",
|
|
||||||
},
|
|
||||||
"data.approved.support.q2": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.approved.support.q2",
|
|
||||||
},
|
|
||||||
"data.approved.support.q3": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.approved.support.q3",
|
|
||||||
},
|
|
||||||
"data.approved.support.q4": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.approved.support.q4",
|
|
||||||
},
|
|
||||||
"data.approved.support.year": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.approved.support.year",
|
|
||||||
},
|
|
||||||
"data.approved.development.q1": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.approved.development.q1",
|
|
||||||
},
|
|
||||||
"data.approved.development.q2": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.approved.development.q2",
|
|
||||||
},
|
|
||||||
"data.approved.development.q3": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.approved.development.q3",
|
|
||||||
},
|
|
||||||
"data.approved.development.q4": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.approved.development.q4",
|
|
||||||
},
|
|
||||||
"data.approved.development.year": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.approved.development.year",
|
|
||||||
},
|
|
||||||
"data.fact.support.q1": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.fact.support.q1",
|
|
||||||
},
|
|
||||||
"data.fact.support.q2": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.fact.support.q2",
|
|
||||||
},
|
|
||||||
"data.fact.support.q3": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.fact.support.q3",
|
|
||||||
},
|
|
||||||
"data.fact.support.q4": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.fact.support.q4",
|
|
||||||
},
|
|
||||||
"data.fact.support.year": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.fact.support.year",
|
|
||||||
},
|
|
||||||
"data.fact.development.q1": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.fact.development.q1",
|
|
||||||
},
|
|
||||||
"data.fact.development.q2": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.fact.development.q2",
|
|
||||||
},
|
|
||||||
"data.fact.development.q3": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.fact.development.q3",
|
|
||||||
},
|
|
||||||
"data.fact.development.q4": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.fact.development.q4",
|
|
||||||
},
|
|
||||||
"data.fact.development.year": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.fact.development.year",
|
|
||||||
},
|
|
||||||
"data.corrected.support.q2": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.corrected.support.q2",
|
|
||||||
},
|
|
||||||
"data.corrected.support.q3": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.corrected.support.q3",
|
|
||||||
},
|
|
||||||
"data.corrected.support.q4": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.corrected.support.q4",
|
|
||||||
},
|
|
||||||
"data.corrected.development.q2": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.corrected.development.q2",
|
|
||||||
},
|
|
||||||
"data.corrected.development.q3": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.corrected.development.q3",
|
|
||||||
},
|
|
||||||
"data.corrected.development.q4": {
|
|
||||||
color_type: greenColumn,
|
|
||||||
accessorKey: "data.corrected.development.q4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: "№ п/п",
|
|
||||||
accessorKey: "pp",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: "",
|
|
||||||
accessorKey: "pp_field",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: "",
|
|
||||||
accessorKey: "data.section_code",
|
|
||||||
columnLetter: "B",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Перечень расходов и затрат",
|
|
||||||
accessorKey: "perechen_raskhodov_i_zatrat",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: "",
|
|
||||||
accessorKey: "perechen_raskhodov_i_zatrat_field",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: "",
|
|
||||||
accessorKey: "data.name",
|
|
||||||
columnLetter: "C",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Расходы, планируемые ССП на 2026 год",
|
|
||||||
accessorKey: "raskhody_planiruemye_ssp_na_2026_god",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: "Поддержка",
|
|
||||||
accessorKey: "raskhody_planiruemye_ssp_na_2026_god_podderzhka",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: "I квартал",
|
|
||||||
accessorKey:
|
|
||||||
"raskhody_planiruemye_ssp_na_2026_god_podderzhka_i_kvartal",
|
|
||||||
accessorKey: "data.plan.support.q1",
|
|
||||||
columnLetter: "D",
|
|
||||||
size: 150,
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "II квартал",
|
|
||||||
accessorKey:
|
|
||||||
"raskhody_planiruemye_ssp_na_2026_god_podderzhka_ii_kvartal",
|
|
||||||
accessorKey: "data.plan.support.q2",
|
|
||||||
columnLetter: "E",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "III квартал",
|
|
||||||
accessorKey:
|
|
||||||
"raskhody_planiruemye_ssp_na_2026_god_podderzhka_iii_kvartal",
|
|
||||||
accessorKey: "data.plan.support.q3",
|
|
||||||
columnLetter: "F",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "IV квартал",
|
|
||||||
|
|
||||||
accessorKey: "data.plan.support.q4",
|
|
||||||
columnLetter: "G",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "ИТОГОПоддержка",
|
|
||||||
|
|
||||||
accessorKey: "data.plan.support.year",
|
|
||||||
columnLetter: "H",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Развитие",
|
|
||||||
accessorKey: "raskhody_planiruemye_ssp_na_2026_god_razvitie",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: "I квартал",
|
|
||||||
accessorKey: "data.plan.development.q1",
|
|
||||||
columnLetter: "I",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "II квартал",
|
|
||||||
accessorKey: "data.plan.development.q2",
|
|
||||||
columnLetter: "J",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "III квартал",
|
|
||||||
accessorKey: "data.plan.development.q3",
|
|
||||||
columnLetter: "K",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "IV квартал",
|
|
||||||
accessorKey: "data.plan.development.q4",
|
|
||||||
columnLetter: "L",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "ИТОГОРазвитие",
|
|
||||||
accessorKey: "data.plan.development.year",
|
|
||||||
columnLetter: "M",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
header: "Утвержденные базовые расходы на 2026 год",
|
|
||||||
accessorKey: "utverzhdennye_bazovye_raskhody_na_2026_god",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: "Поддержка",
|
|
||||||
accessorKey:
|
|
||||||
"utverzhdennye_bazovye_raskhody_na_2026_god_podderzhka",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: "I квартал",
|
|
||||||
accessorKey: "data.approved.support.q1",
|
|
||||||
columnLetter: "P",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "II квартал",
|
|
||||||
accessorKey: "data.approved.support.q2",
|
|
||||||
columnLetter: "Q",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "III квартал",
|
|
||||||
accessorKey: "data.approved.support.q3",
|
|
||||||
columnLetter: "R",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "IV квартал",
|
|
||||||
accessorKey: "data.approved.support.q4",
|
|
||||||
columnLetter: "S",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "ИТОГОПоддержка",
|
|
||||||
accessorKey: "data.approved.support.year",
|
|
||||||
columnLetter: "T",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Развитие",
|
|
||||||
accessorKey: "utverzhdennye_bazovye_raskhody_na_2026_god_razvitie",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: "I квартал",
|
|
||||||
accessorKey: "data.approved.development.q1",
|
|
||||||
columnLetter: "U",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "II квартал",
|
|
||||||
accessorKey: "data.approved.development.q2",
|
|
||||||
columnLetter: "V",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "III квартал",
|
|
||||||
accessorKey: "data.approved.development.q3",
|
|
||||||
columnLetter: "W",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "IV квартал",
|
|
||||||
accessorKey: "data.approved.development.q4",
|
|
||||||
columnLetter: "X",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "ИТОГОРазвитие",
|
|
||||||
accessorKey: "data.approved.development.year",
|
|
||||||
columnLetter: "Y",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Фактические расходы за 2026 год",
|
|
||||||
accessorKey: "fakticheskie_raskhody_za_2026_god",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: "Поддержка",
|
|
||||||
accessorKey: "fakticheskie_raskhody_za_2026_god_podderzhka",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: "I квартал",
|
|
||||||
accessorKey: "data.fact.support.q1",
|
|
||||||
columnLetter: "AB",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "II квартал",
|
|
||||||
accessorKey: "data.fact.support.q2",
|
|
||||||
columnLetter: "AC",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "III квартал",
|
|
||||||
accessorKey: "data.fact.support.q3",
|
|
||||||
columnLetter: "AD",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "IV квартал",
|
|
||||||
accessorKey: "data.fact.support.q4",
|
|
||||||
columnLetter: "AE",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "ИТОГОПоддержка",
|
|
||||||
accessorKey: "data.fact.support.year",
|
|
||||||
columnLetter: "AF",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Развитие",
|
|
||||||
accessorKey: "fakticheskie_raskhody_za_2026_god_razvitie",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: "I квартал",
|
|
||||||
accessorKey: "data.fact.development.q1",
|
|
||||||
columnLetter: "AG",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "II квартал",
|
|
||||||
accessorKey: "data.fact.development.q2",
|
|
||||||
columnLetter: "AH",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "III квартал",
|
|
||||||
accessorKey: "data.fact.development.q3",
|
|
||||||
columnLetter: "AI",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "IV квартал",
|
|
||||||
accessorKey: "data.fact.development.q4",
|
|
||||||
columnLetter: "AJ",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "ИТОГОРазвитие",
|
|
||||||
accessorKey: "data.fact.development.year",
|
|
||||||
columnLetter: "AK",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Скорректированная смета расходов на 2026 год",
|
|
||||||
accessorKey: "skorrektirovannaya_smeta_raskhodov_na_2026_god",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: "Поддержка",
|
|
||||||
accessorKey:
|
|
||||||
"skorrektirovannaya_smeta_raskhodov_na_2026_god_podderzhka",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: "II квартал",
|
|
||||||
accessorKey: "data.corrected.support.q2",
|
|
||||||
columnLetter: "AN",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "III квартал",
|
|
||||||
accessorKey: "data.corrected.support.q3",
|
|
||||||
columnLetter: "AO",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "IV квартал",
|
|
||||||
accessorKey: "data.corrected.support.q4",
|
|
||||||
columnLetter: "AP",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Развитие",
|
|
||||||
accessorKey:
|
|
||||||
"skorrektirovannaya_smeta_raskhodov_na_2026_god_razvitie",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: "II квартал",
|
|
||||||
accessorKey: "data.corrected.development.q2",
|
|
||||||
columnLetter: "AQ",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "III квартал",
|
|
||||||
accessorKey: "data.corrected.development.q3",
|
|
||||||
columnLetter: "AR",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "IV квартал",
|
|
||||||
accessorKey: "data.corrected.development.q4",
|
|
||||||
columnLetter: "AS",
|
|
||||||
size: 150,
|
|
||||||
filterFn: "contains",
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
muiTableHeadCellProps: {
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "#BFBFBF",
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -45,18 +45,6 @@ export const getTableHeadCellStyles = () => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getTableBodyCellProps = ({ column, table }) => {
|
|
||||||
const isPinned = (table.getState().columnPinning?.left || []).includes(column.id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
'data-col-id': column.id,
|
|
||||||
sx: {
|
|
||||||
...getTableHeadCellStyles().sx,
|
|
||||||
...(isPinned && { zIndex: 5 }),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTablePaperStyles = () => ({
|
export const getTablePaperStyles = () => ({
|
||||||
sx: {
|
sx: {
|
||||||
boxShadow: "none",
|
boxShadow: "none",
|
||||||
|
|||||||
@ -25,29 +25,17 @@ export const RealtimeProvider = ({
|
|||||||
formId,
|
formId,
|
||||||
sheetName,
|
sheetName,
|
||||||
direction,
|
direction,
|
||||||
year,
|
|
||||||
userId,
|
userId,
|
||||||
isProject = false,
|
|
||||||
}) => {
|
}) => {
|
||||||
// Формируем URL (кастомные секреты на фронте пока недоступны, делаем через REACT_APP_ROOT_PATH)
|
// Формируем URL (кастомные секреты на фронте пока недоступны, делаем через REACT_APP_ROOT_PATH)
|
||||||
const wsUrl =
|
const wsUrl = `${(process.env.REACT_APP_API_URL || process.env.REACT_APP_API_URL || process.env.REACT_APP_ROOT_PATH || "ws://localhost:8000").replace(/^http/, "ws")}/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ""}`;
|
||||||
`${(
|
|
||||||
process.env.REACT_APP_API_URL ||
|
|
||||||
process.env.REACT_APP_API_URL ||
|
|
||||||
process.env.REACT_APP_ROOT_PATH ||
|
|
||||||
"ws://localhost:8000"
|
|
||||||
).replace(/^http/, "ws")}` +
|
|
||||||
(!isProject
|
|
||||||
? `/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ""}`
|
|
||||||
: `/api/v1/ws/projects/${formId}/report/${year}/${sheetName}`);
|
|
||||||
|
|
||||||
//для ячеек которые редактируются другими пользователями
|
//для ячеек которые редактируются другими пользователями
|
||||||
const [lockedCells, setLockedCells] = useState([]);
|
const [lockedCells, setLockedCells] = useState([]);
|
||||||
const handleMessage = useCallback((data) => {
|
const handleMessage = useCallback((data) => {
|
||||||
// Обработка ошибок
|
// Обработка ошибок
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
console.log(data.error.message);
|
console.log(data.error.message)
|
||||||
toast.error("Server error:" + data.error.message || "");
|
toast.error("Server error:" + data.error.message || '');
|
||||||
onErrorRef.current?.(data.error);
|
onErrorRef.current?.(data.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -93,7 +81,7 @@ export const RealtimeProvider = ({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { isConnectedRef, isConnected, sendMessage, disconnect, lastError } = useWebSocket(
|
const { isConnected, sendMessage, disconnect, lastError } = useWebSocket(
|
||||||
wsUrl,
|
wsUrl,
|
||||||
handleMessage,
|
handleMessage,
|
||||||
);
|
);
|
||||||
@ -157,26 +145,13 @@ export const RealtimeProvider = ({
|
|||||||
}
|
}
|
||||||
}, [isConnected]);
|
}, [isConnected]);
|
||||||
|
|
||||||
const addCommonField = (message) => {
|
|
||||||
if (isProject) {
|
|
||||||
message.report_type = sheetName;
|
|
||||||
message.project_id = formId;
|
|
||||||
message.year = year;
|
|
||||||
} else {
|
|
||||||
message.sheet = sheetName;
|
|
||||||
message.form_id = formId;
|
|
||||||
message.direction = direction;
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция для начала редактирования ячейки
|
// Функция для начала редактирования ячейки
|
||||||
const startEditing = useCallback(
|
const startEditing = useCallback(
|
||||||
async (row, column) => {
|
async (row, column) => {
|
||||||
const columnId = column.id;
|
const columnId = column.id;
|
||||||
const rowId = row.id;
|
const rowId = row.id;
|
||||||
|
|
||||||
if (!isConnectedRef) {
|
if (!isConnected) {
|
||||||
onErrorRef.current?.("WebSocket не подключен");
|
onErrorRef.current?.("WebSocket не подключен");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -208,7 +183,7 @@ export const RealtimeProvider = ({
|
|||||||
const columnId = column.id;
|
const columnId = column.id;
|
||||||
const rowId = row.id;
|
const rowId = row.id;
|
||||||
|
|
||||||
if (!isConnectedRef) {
|
if (!isConnected) {
|
||||||
onErrorRef.current?.("WebSocket не подключен");
|
onErrorRef.current?.("WebSocket не подключен");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -238,7 +213,7 @@ export const RealtimeProvider = ({
|
|||||||
async (row, column, value) => {
|
async (row, column, value) => {
|
||||||
const columnId = column.id;
|
const columnId = column.id;
|
||||||
const rowId = row.id;
|
const rowId = row.id;
|
||||||
if (!isConnectedRef) {
|
if (!isConnected) {
|
||||||
onErrorRef.current?.("WebSocket не подключен");
|
onErrorRef.current?.("WebSocket не подключен");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -249,7 +224,6 @@ export const RealtimeProvider = ({
|
|||||||
event: "cell_updated",
|
event: "cell_updated",
|
||||||
data: {
|
data: {
|
||||||
line_id: lineId,
|
line_id: lineId,
|
||||||
line_id_code: row.id,
|
|
||||||
column: colId,
|
column: colId,
|
||||||
value: value,
|
value: value,
|
||||||
},
|
},
|
||||||
@ -267,7 +241,7 @@ export const RealtimeProvider = ({
|
|||||||
|
|
||||||
const addRow = useCallback(
|
const addRow = useCallback(
|
||||||
async (rowData) => {
|
async (rowData) => {
|
||||||
if (!isConnectedRef) {
|
if (!isConnected) {
|
||||||
onErrorRef.current?.("Нет подключения или авторизации");
|
onErrorRef.current?.("Нет подключения или авторизации");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -275,9 +249,10 @@ export const RealtimeProvider = ({
|
|||||||
const message = {
|
const message = {
|
||||||
event: "row_added",
|
event: "row_added",
|
||||||
data: rowData,
|
data: rowData,
|
||||||
|
sheet: sheetName,
|
||||||
|
form_id: formId,
|
||||||
|
direction: direction,
|
||||||
};
|
};
|
||||||
addCommonField(message);
|
|
||||||
|
|
||||||
return sendMessage(message);
|
return sendMessage(message);
|
||||||
},
|
},
|
||||||
[isConnected, sendMessage, sheetName, formId, direction],
|
[isConnected, sendMessage, sheetName, formId, direction],
|
||||||
@ -285,7 +260,7 @@ export const RealtimeProvider = ({
|
|||||||
|
|
||||||
const deleteRow = useCallback(
|
const deleteRow = useCallback(
|
||||||
async (rowId) => {
|
async (rowId) => {
|
||||||
if (!isConnectedRef) {
|
if (!isConnected) {
|
||||||
onErrorRef.current?.("Нет подключения или авторизации");
|
onErrorRef.current?.("Нет подключения или авторизации");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -293,8 +268,10 @@ export const RealtimeProvider = ({
|
|||||||
const message = {
|
const message = {
|
||||||
event: "row_deleted",
|
event: "row_deleted",
|
||||||
data: { row_id: rowId },
|
data: { row_id: rowId },
|
||||||
|
sheet: sheetName,
|
||||||
|
form_id: formId,
|
||||||
|
direction: direction,
|
||||||
};
|
};
|
||||||
addCommonField(message);
|
|
||||||
|
|
||||||
return sendMessage(message);
|
return sendMessage(message);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -41,7 +41,6 @@ export const useColumnSettings = (tableKey) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleUnpinColumn = useCallback((colId) => {
|
const handleUnpinColumn = useCallback((colId) => {
|
||||||
if (!colId) return;
|
|
||||||
if (columnPinningDefault.left.includes(colId)) return;
|
if (columnPinningDefault.left.includes(colId)) return;
|
||||||
setColumnPinning((prev) => ({
|
setColumnPinning((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|||||||
@ -4,9 +4,8 @@ import { useRealtime } from "../contexts/RealtimeContext";
|
|||||||
import { deleteRow, insertCell, updateCells } from "../utils/cellUtils";
|
import { deleteRow, insertCell, updateCells } from "../utils/cellUtils";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { isVspNewRow, isVspRow } from "../utils/rowUtils";
|
import { isVspNewRow, isVspRow } from "../utils/rowUtils";
|
||||||
import { ProjectsApi } from "../../../api/projects";
|
|
||||||
|
|
||||||
const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
const useRealtimeData = (formId, sheetName, direction) => {
|
||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState([]);
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
@ -78,10 +77,7 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
let res;
|
let res;
|
||||||
|
if (direction !== null) {
|
||||||
if (formType == "PROJECT") {
|
|
||||||
res = await ProjectsApi.getTableData(formId, sheetName, year);
|
|
||||||
} else if (direction !== null) {
|
|
||||||
res = await FormsSheetApi.get(formId, sheetName, {
|
res = await FormsSheetApi.get(formId, sheetName, {
|
||||||
params: { direction: direction },
|
params: { direction: direction },
|
||||||
});
|
});
|
||||||
|
|||||||
@ -107,16 +107,30 @@ export const useWebSocket = (url, onMessage) => {
|
|||||||
return false;
|
return false;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const login = useCallback(
|
||||||
|
(token) => {
|
||||||
|
if (!token) {
|
||||||
|
console.warn("No token provided for login");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return sendMessage({
|
||||||
|
event: "user_login",
|
||||||
|
data: { token },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[sendMessage],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
connect();
|
connect();
|
||||||
return () => disconnect();
|
return () => disconnect();
|
||||||
}, [connect, disconnect]);
|
}, [connect, disconnect]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isConnectedRef,
|
isConnected: isConnectedRef,
|
||||||
isConnected,
|
|
||||||
error,
|
error,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
|
login,
|
||||||
disconnect,
|
disconnect,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
import { columns as mockColumns } from '../../mocks/mockTable';
|
||||||
|
import { config } from './constants/FORM_2/AHR';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { useRealtime } from './contexts/RealtimeContext';
|
|
||||||
|
|
||||||
const EditCellPortal = React.memo(({
|
function EditCellPortal({
|
||||||
cell,
|
cell,
|
||||||
table,
|
table,
|
||||||
EditCell,
|
EditCell,
|
||||||
@ -11,71 +12,85 @@ const EditCellPortal = React.memo(({
|
|||||||
onSaveEnd,
|
onSaveEnd,
|
||||||
onError,
|
onError,
|
||||||
isEditable
|
isEditable
|
||||||
}) => {
|
}) {
|
||||||
const { formId, formType, sheetName, direction } = useParams();
|
const { formId, formType, sheetName, direction } = useParams();
|
||||||
const tableKey = `${formId}_${formType}_${sheetName}_${direction}`;
|
const tableKey = `${formId}_${formType}_${sheetName}_${direction}`;
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const [refTbody, setRefTbody] = useState(null);
|
const [refTbody, setRefTbody] = useState(false);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const isDisabled = false;
|
||||||
const { endEditing: contextEndEditing } = useRealtime();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
const tbodyRef = ref.current.offsetParent?.offsetParent?.offsetParent;
|
const tbodyRef = ref.current.offsetParent?.offsetParent?.offsetParent;
|
||||||
if (tbodyRef && tbodyRef !== refTbody) {
|
setRefTbody(tbodyRef);
|
||||||
setRefTbody(tbodyRef);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleChange = useCallback( (val) => {
|
const handleChange = async (val) => {
|
||||||
table.setEditingCell(null);
|
table.setEditingCell(null);
|
||||||
try {
|
try {
|
||||||
if (table.options.meta?.updateCell) {
|
if (table.options.meta?.updateCell) {
|
||||||
const success = table.options.meta.updateCell(
|
const success = await table.options.meta.updateCell(
|
||||||
cell.row,
|
cell.row,
|
||||||
cell.column,
|
cell.column,
|
||||||
val
|
val
|
||||||
);
|
);
|
||||||
if (!success) {
|
if (success) {
|
||||||
|
console.log('Cell updated successfully via WebSocket');
|
||||||
|
} else {
|
||||||
console.error('Failed to update cell via WebSocket');
|
console.error('Failed to update cell via WebSocket');
|
||||||
onError?.('Не удалось сохранить изменение');
|
const errorMsg = 'Не удалось сохранить изменение';
|
||||||
|
onError?.(errorMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating cell:', error);
|
console.error('Error updating cell:', error);
|
||||||
onError?.(error.message);
|
onError?.(error.message);
|
||||||
}
|
}
|
||||||
}, [table, cell, onError]);
|
};
|
||||||
|
|
||||||
const editCellProps = useMemo(() => ({
|
|
||||||
refCell: ref,
|
|
||||||
cell,
|
|
||||||
value: cell.getValue(),
|
|
||||||
disabled: !isEditable,
|
|
||||||
onChange: handleChange,
|
|
||||||
tableId: tableKey,
|
|
||||||
table,
|
|
||||||
cellId: `${cell.row.id}_${cell.column.id}`,
|
|
||||||
}), [cell, isEditable, handleChange, tableKey, table]);
|
|
||||||
|
|
||||||
const portalContent = useMemo(() => {
|
|
||||||
if (!refTbody || isSaving) return null;
|
|
||||||
return createPortal(
|
|
||||||
<EditCell {...editCellProps} />,
|
|
||||||
refTbody
|
|
||||||
);
|
|
||||||
}, [refTbody, isSaving, editCellProps, EditCell]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={ref} style={{ width: '100%', height: '100%' }} />
|
<div
|
||||||
{portalContent}
|
ref={ref}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{refTbody && !isSaving &&
|
||||||
|
createPortal(
|
||||||
|
<EditCell
|
||||||
|
refCell={ref}
|
||||||
|
cell={cell}
|
||||||
|
value={cell.getValue()}
|
||||||
|
disabled={!isEditable}
|
||||||
|
onChange={handleChange}
|
||||||
|
tableId={tableKey}
|
||||||
|
cellId={cell.row.id + '_' + cell.column.id}
|
||||||
|
/>,
|
||||||
|
refTbody,
|
||||||
|
)}
|
||||||
|
{isSaving && (
|
||||||
|
createPortal(
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
background: 'rgba(0,0,0,0.5)',
|
||||||
|
color: 'white',
|
||||||
|
padding: '4px 8px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '12px',
|
||||||
|
zIndex: 1000
|
||||||
|
}}>
|
||||||
|
Saving...
|
||||||
|
</div>,
|
||||||
|
refTbody
|
||||||
|
)
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
|
||||||
export const getTableColumns = ({
|
export const getTableColumns = ({
|
||||||
Cell,
|
Cell,
|
||||||
@ -85,105 +100,43 @@ export const getTableColumns = ({
|
|||||||
onCellNumberClick,
|
onCellNumberClick,
|
||||||
}) => {
|
}) => {
|
||||||
const columnColors = { ...columnsConfig.colors };
|
const columnColors = { ...columnsConfig.colors };
|
||||||
const columns = structuredClone(columnsConfig.columns);
|
const columns = [...columnsConfig.columns];
|
||||||
|
|
||||||
const cellPropsCache = new WeakMap();
|
const processColumns = (columns, EditCell, Cell) => {
|
||||||
const editPropsCache = new WeakMap();
|
|
||||||
|
|
||||||
const getCachedCellProps = (row, column, table) => {
|
|
||||||
const key = `${row.id}_${column.id}`;
|
|
||||||
|
|
||||||
if (!cellPropsCache.has(row)) {
|
|
||||||
cellPropsCache.set(row, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowCache = cellPropsCache.get(row);
|
|
||||||
if (rowCache[key]) {
|
|
||||||
const cached = rowCache[key];
|
|
||||||
const currentGlobalFilter = table.getState().globalFilter;
|
|
||||||
const currentColumnFilter = table.getState().columnFilters?.find(f => f.id === column.id)?.value;
|
|
||||||
|
|
||||||
if (cached.globalFilter === currentGlobalFilter &&
|
|
||||||
cached.columnFilter === currentColumnFilter) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const globalFilter = table.getState().globalFilter;
|
|
||||||
const columnFilter = table.getState().columnFilters?.find(f => f.id === column.id)?.value;
|
|
||||||
|
|
||||||
const props = {
|
|
||||||
globalFilter,
|
|
||||||
columnFilter,
|
|
||||||
isEditable: row.original?.row_type === 'INPUT' || false,
|
|
||||||
backgroundColor: columnColors[column.id]?.color_type?.[row.original?.row_type || row.row_type],
|
|
||||||
isUpdating: table.options.meta?.updatingCells?.[`${row.id}_${column.id}`],
|
|
||||||
_hash: `${globalFilter}_${columnFilter}_${row.id}_${column.id}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
rowCache[key] = props;
|
|
||||||
return props;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCachedEditProps = (row, column, table) => {
|
|
||||||
const key = `${row.id}_${column.id}`;
|
|
||||||
|
|
||||||
if (!editPropsCache.has(row)) {
|
|
||||||
editPropsCache.set(row, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowCache = editPropsCache.get(row);
|
|
||||||
if (rowCache[key]) {
|
|
||||||
return rowCache[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = {
|
|
||||||
isEditable: row.original?.row_type === 'INPUT' || false,
|
|
||||||
};
|
|
||||||
|
|
||||||
rowCache[key] = props;
|
|
||||||
return props;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Оптимизированная функция processColumns
|
|
||||||
const processColumns = (columns) => {
|
|
||||||
columns.forEach((col) => {
|
columns.forEach((col) => {
|
||||||
col.Cell = ({ cell, table, column, row }) => {
|
col.Cell = ({ cell, table, column, row }) => (
|
||||||
const props = getCachedCellProps(row, column, table);
|
<Cell
|
||||||
|
row={row}
|
||||||
|
column={column}
|
||||||
|
cell={cell}
|
||||||
|
globalFilter={
|
||||||
|
table.getState().globalFilter
|
||||||
|
}
|
||||||
|
isEditable={row.original?.row_type === 'INPUT' || false}
|
||||||
|
backgroundColor={columnColors[column.id]?.color_type?.[row.original?.row_type || row.row_type]}
|
||||||
|
color={columnColors[column.id]?.color_type?.['COLOR']}
|
||||||
|
isUpdating={table.options.meta?.updatingCells?.[`${row.id}_${column.id}`]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const cellProps = useMemo(() => ({
|
col.Edit = ({ cell, table, row, column }) => (
|
||||||
row,
|
<EditCellPortal
|
||||||
column,
|
cell={cell}
|
||||||
cell,
|
table={table}
|
||||||
...props,
|
EditCell={EditCell}
|
||||||
}), [row.id, column.id, cell.getValue(), props._hash]);
|
onError={onCellUpdateError}
|
||||||
|
isEditable={row.original?.row_type === 'INPUT' || false}
|
||||||
return <Cell {...cellProps} />;
|
/>
|
||||||
};
|
);
|
||||||
|
|
||||||
col.Edit = ({ cell, table, row, column }) => {
|
|
||||||
const props = getCachedEditProps(row, column, table);
|
|
||||||
|
|
||||||
const editComponent = useMemo(() => (
|
|
||||||
<EditCellPortal
|
|
||||||
cell={cell}
|
|
||||||
table={table}
|
|
||||||
EditCell={EditCell}
|
|
||||||
onError={onCellUpdateError}
|
|
||||||
isEditable={props.isEditable}
|
|
||||||
/>
|
|
||||||
), [cell, table, EditCell, onCellUpdateError, props.isEditable]);
|
|
||||||
|
|
||||||
return editComponent;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Рекурсивно обрабатываем вложенные колонки
|
||||||
if (col.columns?.length) {
|
if (col.columns?.length) {
|
||||||
processColumns(col.columns);
|
processColumns(col.columns, EditCell, Cell);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
processColumns(columns);
|
processColumns(columns, EditCell, Cell);
|
||||||
|
|
||||||
const rowNumber = {
|
const rowNumber = {
|
||||||
accessorKey: 'sort_order',
|
accessorKey: 'sort_order',
|
||||||
@ -191,11 +144,9 @@ export const getTableColumns = ({
|
|||||||
size: 50,
|
size: 50,
|
||||||
enableEditing: false,
|
enableEditing: false,
|
||||||
Cell: ({ cell, row }) => {
|
Cell: ({ cell, row }) => {
|
||||||
const handleClick = useCallback(() => {
|
return <button onClick={() => { onCellNumberClick(row.id) }}>
|
||||||
onCellNumberClick(row.id);
|
{cell.getValue() + ' '}
|
||||||
}, [row.id, onCellNumberClick]);
|
</button>
|
||||||
|
|
||||||
return <button onClick={handleClick}>{cell.getValue() + ' '}</button>;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { getDataRowId } from "./rowUtils";
|
|
||||||
|
|
||||||
export function setNewValueToData(data, colId, rowId, value) {
|
export function setNewValueToData(data, colId, rowId, value) {
|
||||||
let targetObject = data;
|
let targetObject = data;
|
||||||
if (rowId && rowId.length) {
|
if (rowId && rowId.length) {
|
||||||
@ -46,29 +44,6 @@ export function setNewValueToData(data, colId, rowId, value) {
|
|||||||
export function updateCells(data, newRows) {
|
export function updateCells(data, newRows) {
|
||||||
const updatedRows = JSON.parse(JSON.stringify(data));
|
const updatedRows = JSON.parse(JSON.stringify(data));
|
||||||
|
|
||||||
function deepMerge(target, source) {
|
|
||||||
if (!source || typeof source !== "object") {
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
if (!target || typeof target !== "object") {
|
|
||||||
target = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(source).forEach((key) => {
|
|
||||||
if (
|
|
||||||
source[key] &&
|
|
||||||
typeof source[key] === "object" &&
|
|
||||||
!Array.isArray(source[key])
|
|
||||||
) {
|
|
||||||
target[key] = deepMerge(target[key], source[key]);
|
|
||||||
} else {
|
|
||||||
target[key] = source[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
newRows.forEach((update) => {
|
newRows.forEach((update) => {
|
||||||
const [rowType, depth, sortOrder, newData] = update;
|
const [rowType, depth, sortOrder, newData] = update;
|
||||||
|
|
||||||
@ -86,7 +61,7 @@ export function updateCells(data, newRows) {
|
|||||||
Object.keys(newData).forEach((key) => {
|
Object.keys(newData).forEach((key) => {
|
||||||
if (row.data && row.data[key]) {
|
if (row.data && row.data[key]) {
|
||||||
if (typeof row.data[key] === "object") {
|
if (typeof row.data[key] === "object") {
|
||||||
row.data[key] = deepMerge(row.data[key], newData[key]);
|
row.data[key] = { ...row.data[key], ...newData[key] };
|
||||||
} else {
|
} else {
|
||||||
row.data[key] = newData[key];
|
row.data[key] = newData[key];
|
||||||
}
|
}
|
||||||
@ -115,7 +90,6 @@ export function updateCells(data, newRows) {
|
|||||||
|
|
||||||
return updatedRows;
|
return updatedRows;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function insertCell(data, newRow, expense_item_id) {
|
export function insertCell(data, newRow, expense_item_id) {
|
||||||
const updatedRows = JSON.parse(JSON.stringify(data));
|
const updatedRows = JSON.parse(JSON.stringify(data));
|
||||||
const [row_type, depth, sort_order, dataRow] = newRow;
|
const [row_type, depth, sort_order, dataRow] = newRow;
|
||||||
@ -133,30 +107,24 @@ export function insertCell(data, newRow, expense_item_id) {
|
|||||||
return updatedRows;
|
return updatedRows;
|
||||||
}
|
}
|
||||||
|
|
||||||
let adding = false;
|
|
||||||
function findAndUpdate(rows) {
|
function findAndUpdate(rows) {
|
||||||
for (let i = 0; i < rows.length; i++) {
|
for (let i = 0; i < rows.length; i++) {
|
||||||
const row = rows[i];
|
const row = rows[i];
|
||||||
|
|
||||||
if (
|
if (row.data.header.expense_item_id === expense_item_id) {
|
||||||
(row.sort_order >= newRowObject.sort_order) &
|
|
||||||
(row.data.line_id != newRowObject.data.line_id)
|
|
||||||
) {
|
|
||||||
row.sort_order = row.sort_order + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!adding & (row.data.header.expense_item_id === expense_item_id)) {
|
|
||||||
if (!("subRows" in row)) {
|
if (!("subRows" in row)) {
|
||||||
row.subRows = [];
|
row.subRows = [];
|
||||||
}
|
}
|
||||||
row.subRows.push(newRowObject);
|
row.subRows.push(newRowObject);
|
||||||
adding = true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row.subRows && Array.isArray(row.subRows) && row.subRows.length > 0) {
|
if (row.subRows && Array.isArray(row.subRows) && row.subRows.length > 0) {
|
||||||
const found = findAndUpdate(row.subRows);
|
const found = findAndUpdate(row.subRows);
|
||||||
|
if (found) return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
findAndUpdate(updatedRows);
|
findAndUpdate(updatedRows);
|
||||||
return updatedRows;
|
return updatedRows;
|
||||||
@ -165,26 +133,21 @@ export function insertCell(data, newRow, expense_item_id) {
|
|||||||
export function deleteRow(data, lineId) {
|
export function deleteRow(data, lineId) {
|
||||||
const updatedRows = JSON.parse(JSON.stringify(data));
|
const updatedRows = JSON.parse(JSON.stringify(data));
|
||||||
|
|
||||||
let rowDeleteSortOrder = false;
|
|
||||||
function findAndDelete(rows) {
|
function findAndDelete(rows) {
|
||||||
for (let i = 0; i < rows.length; i++) {
|
for (let i = 0; i < rows.length; i++) {
|
||||||
const row = rows[i];
|
const row = rows[i];
|
||||||
const rowId = getDataRowId(row.data);
|
|
||||||
|
|
||||||
if (rowId == lineId) {
|
if (row.data.line_id == lineId) {
|
||||||
rowDeleteSortOrder = row.sort_order;
|
|
||||||
rows.splice(i, 1);
|
rows.splice(i, 1);
|
||||||
i--;
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
if ((rowDeleteSortOrder > 0) & (row.sort_order >= rowDeleteSortOrder)) {
|
|
||||||
row.sort_order = row.sort_order - 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row.subRows && Array.isArray(row.subRows) && row.subRows.length > 0) {
|
if (row.subRows && Array.isArray(row.subRows) && row.subRows.length > 0) {
|
||||||
findAndDelete(row.subRows);
|
const found = findAndDelete(row.subRows);
|
||||||
|
if (found) return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
findAndDelete(updatedRows);
|
findAndDelete(updatedRows);
|
||||||
return updatedRows;
|
return updatedRows;
|
||||||
|
|||||||
@ -1,9 +1,5 @@
|
|||||||
export function getDataRowId(data) {
|
|
||||||
return data?.id || data?.line_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRowId(row) {
|
export function getRowId(row) {
|
||||||
return getDataRowId(row.original.data);
|
return row.original.data.id || row.original.data.line_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isVspRow(row) {
|
export function isVspRow(row) {
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import { StageStatusChip } from './StageStatusChip';
|
|||||||
import { formatDateForApi } from './utils';
|
import { formatDateForApi } from './utils';
|
||||||
import CollapsibleTree from '../common/CollapsibleTree';
|
import CollapsibleTree from '../common/CollapsibleTree';
|
||||||
import { collectLeafColumns } from '../RealtimeTable/utils/columnUtils';
|
import { collectLeafColumns } from '../RealtimeTable/utils/columnUtils';
|
||||||
import { STAGE_ROLE_RUSSIAN_NAME } from '../../constants/constants';
|
import { STAGE_ROLE_RUSSIAN_NAME } from '../../constants';
|
||||||
|
|
||||||
export const AddStagesModal = ({
|
export const AddStagesModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Box, Divider, Stack } from '@mui/material';
|
import { Box, Divider, Stack } from '@mui/material';
|
||||||
import { UnLockedSvg } from '../common/icons/icons';
|
import { UnLockedSvg } from '../common/icons/icons';
|
||||||
import { ROLES_ID_RUSSIAN_NAME, EDIT_ACCESS_RUSSIAN } from '../../constants/constants';
|
import { ROLES_ID_RUSSIAN_NAME, EDIT_ACCESS_RUSSIAN } from '../../constants';
|
||||||
|
|
||||||
export const StageInfoForTooltip = ({ stageData = [] }) => {
|
export const StageInfoForTooltip = ({ stageData = [] }) => {
|
||||||
const accessData = stageData[0] || [];
|
const accessData = stageData[0] || [];
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user