Compare commits
110 Commits
8c88acab49
...
0b451b2499
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b451b2499 | |||
| 3a2bb6c8aa | |||
| 45710ff58e | |||
| efe6df992d | |||
| 15477cf6ce | |||
| 68fd2a7646 | |||
| 18ec053e44 | |||
|
|
e4726b6284 | ||
| e6133b82fe | |||
|
|
3c7aafc8bb | ||
| ed0e7c805b | |||
| 9f652e5b94 | |||
| 0aa053c0c4 | |||
| 74d191ba20 | |||
| c976ae8c92 | |||
| 28ce0c1511 | |||
|
|
225c68c4ac | ||
| 7c64938020 | |||
| ad7b374fec | |||
| ab77ce458d | |||
| 12d0be1351 | |||
| 79bf3231b9 | |||
|
|
34d5fbc247 | ||
|
|
096a8b817f | ||
|
|
dc621a1223 | ||
| 022aa7b186 | |||
|
|
c7f4fed6f6 | ||
| e5e029e763 | |||
| 520d64c04d | |||
| bad774f162 | |||
| c9346e5219 | |||
|
|
c34825d113 | ||
| 2a88240dd9 | |||
| ab2c1151fa | |||
| c8dbba7e57 | |||
| f27ea41875 | |||
|
|
e06b2dc4d4 | ||
|
|
2f505c7051 | ||
| b07e02e643 | |||
|
|
580d9db311 | ||
| f0358f640d | |||
|
|
27e79a3df5 | ||
| 569138530d | |||
| b664a39469 | |||
| 857827813b | |||
| ec73ab9a2b | |||
| 5537b8229e | |||
|
|
5eb73ddb37 | ||
|
|
4d5a39ef12 | ||
|
|
ccba554b86 | ||
| c4dd35b6bd | |||
| ffe4ff9edb | |||
| 0a99aeed0c | |||
| 8a280781bb | |||
|
|
88e12d1a8b | ||
|
|
6f0b3eb0cc | ||
|
|
97934a3404 | ||
|
|
0b33ba2b40 | ||
|
|
5ddf7479a4 | ||
|
|
bc4e5122c6 | ||
|
|
acd4645a4e | ||
| bdb4c389ce | |||
| 5e6ad47128 | |||
| 19dd443146 | |||
|
|
2cd80343ab | ||
|
|
c0e186baef | ||
|
|
1df748fb03 | ||
|
|
2cc53b6846 | ||
|
|
944fbe4d7e | ||
| 155888f7fc | |||
|
|
019b93092c | ||
|
|
a23a2dffc6 | ||
| 2e781ca809 | |||
|
|
cc62551c92 | ||
|
|
40c373b5ed | ||
| 4ec4936a0f | |||
| 88a9713fb3 | |||
|
|
4e7cc9b3bd | ||
|
|
c8cc1afec1 | ||
| 047cabe14b | |||
| 7fabb22c45 | |||
|
|
5d16915bad | ||
| 66846c6afa | |||
| b283bda007 | |||
|
|
4f8393bedc | ||
| 796640a491 | |||
|
|
03ea64d6b5 | ||
| e91c5e1846 | |||
|
|
8869423ea6 | ||
| c1059736fb | |||
| 7a3ef892cf | |||
|
|
0c10be7eb1 | ||
|
|
48c7ab3eb1 | ||
|
|
20d0520b7d | ||
| 2e5cf3d2af | |||
|
|
3b7c93f5a7 | ||
| 62a2f9bae4 | |||
|
|
e28f6a3887 | ||
|
|
0652ba4aee | ||
|
|
7da934399e | ||
| 3c1722293c | |||
| 905fa98ca5 | |||
| 9c725e9678 | |||
| f26691805a | |||
| 2343b5c9b1 | |||
| 23c5f053bf | |||
| 723a110035 | |||
| 5bfcf1302f | |||
| ee12939515 | |||
| bac2a7a780 |
@ -762,6 +762,43 @@ def upgrade() -> None:
|
|||||||
['code'],
|
['code'],
|
||||||
schema='v3',
|
schema='v3',
|
||||||
)
|
)
|
||||||
|
create_table_if_not_exists(
|
||||||
|
'form3_phase',
|
||||||
|
sa.Column('rf_project_report_id', sa.Integer(),primary_key=True),
|
||||||
|
sa.Column('phase_code', sa.String(), primary_key=True),
|
||||||
|
sa.Column('role', sa.String(), nullable=False),
|
||||||
|
sa.Column('column_keys', postgresql.ARRAY(sa.Text()), nullable=False),
|
||||||
|
sa.Column('opens_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('closes_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
schema='v3',
|
||||||
|
)
|
||||||
|
|
||||||
|
create_check_constraint_if_not_exists(
|
||||||
|
'chk_v3_form3_phase_window', 'form3_phase',
|
||||||
|
sa.text('closes_at > opens_at'),
|
||||||
|
schema='v3',
|
||||||
|
)
|
||||||
|
create_check_constraint_if_not_exists(
|
||||||
|
'chk_v3_form3_phase_columns', 'form3_phase',
|
||||||
|
sa.text('cardinality(column_keys) > 0'),
|
||||||
|
schema='v3',
|
||||||
|
)
|
||||||
|
create_check_constraint_if_not_exists(
|
||||||
|
'chk_v3_form3_phase_no_admin', 'form3_phase',
|
||||||
|
sa.text("role != 'ADMIN'"),
|
||||||
|
schema='v3',
|
||||||
|
)
|
||||||
|
create_index_if_not_exists(
|
||||||
|
'ix_v3_form3_phase_active', 'form3_phase',
|
||||||
|
['rf_project_report_id', 'role', 'opens_at', 'closes_at'],
|
||||||
|
schema='v3',
|
||||||
|
)
|
||||||
|
create_index_if_not_exists(
|
||||||
|
'ix_v3_form3_phase_columns_gin', 'form3_phase',
|
||||||
|
['column_keys'],
|
||||||
|
schema='v3',
|
||||||
|
postgresql_using='gin',
|
||||||
|
)
|
||||||
create_table_if_not_exists(
|
create_table_if_not_exists(
|
||||||
'security_detail',
|
'security_detail',
|
||||||
sa.Column('id', sa.Integer(),
|
sa.Column('id', sa.Integer(),
|
||||||
@ -920,7 +957,6 @@ 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',
|
||||||
|
|||||||
63
api/alembic/versions/0004_vsp_delete_with_close_date.py
Normal file
63
api/alembic/versions/0004_vsp_delete_with_close_date.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
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
|
||||||
330
api/alembic/versions/0005_phase_timestamps.py
Normal file
330
api/alembic/versions/0005_phase_timestamps.py
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
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
|
||||||
79
api/alembic/versions/0006_form3.py
Normal file
79
api/alembic/versions/0006_form3.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
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
|
||||||
180
api/alembic/versions/0007_form_constraints.py
Normal file
180
api/alembic/versions/0007_form_constraints.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
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
|
||||||
16
api/alembic/versions/0008_rent_end_constraint.py
Normal file
16
api/alembic/versions/0008_rent_end_constraint.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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
|
||||||
785
api/alembic/versions/0009_phase3_timestamps.py
Normal file
785
api/alembic/versions/0009_phase3_timestamps.py
Normal file
@ -0,0 +1,785 @@
|
|||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision = "0009"
|
||||||
|
down_revision = "0008"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION v3.copy_template_to_report(p_report_id integer)
|
||||||
|
RETURNS integer
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_report_type VARCHAR;
|
||||||
|
v_inserted INTEGER;
|
||||||
|
v_org_unit INTEGER;
|
||||||
|
v_utc_offset VARCHAR(3);
|
||||||
|
BEGIN
|
||||||
|
SELECT report_type, org_unit_id INTO v_report_type, v_org_unit
|
||||||
|
FROM v3.rf_project_report AS rpr
|
||||||
|
INNER JOIN v3.project AS p ON p.id = rpr.project_id
|
||||||
|
WHERE rpr.id = p_report_id;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'rf_project_report id=% не существует', p_report_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT utc_offset INTO v_utc_offset
|
||||||
|
FROM v3.org_unit
|
||||||
|
WHERE id = v_org_unit;
|
||||||
|
|
||||||
|
INSERT INTO v3.form3_phase
|
||||||
|
(rf_project_report_id, phase_code, role, column_keys, opens_at, closes_at)
|
||||||
|
SELECT p_report_id, pt.phase_code, pt.role,
|
||||||
|
pt.column_keys, (pt.opens_at::text || v_utc_offset)::timestamptz,
|
||||||
|
(pt.closes_at::text || v_utc_offset)::timestamptz
|
||||||
|
FROM v3.phase_template pt
|
||||||
|
WHERE pt.form_type = 'FORM_3'
|
||||||
|
AND pt.sheet = v_report_type
|
||||||
|
ON CONFLICT (rf_project_report_id, phase_code) DO NOTHING;
|
||||||
|
|
||||||
|
GET DIAGNOSTICS v_inserted = ROW_COUNT;
|
||||||
|
RETURN v_inserted;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION v3.add_form3_phase(p_rf_project_report_id integer, p_phase_code character varying, p_role character varying, p_column_keys text[], p_opens_at timestamp without time zone, p_closes_at timestamp without time zone)
|
||||||
|
RETURNS v3.form3_phase
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_phase v3.form3_phase;
|
||||||
|
v_org_unit_id INT;
|
||||||
|
v_org_unit_title VARCHAR;
|
||||||
|
v_year INT;
|
||||||
|
v_task_name TEXT;
|
||||||
|
v_phase_id TEXT;
|
||||||
|
v_utc_offset VARCHAR(3);
|
||||||
|
v_report_type VARCHAR;
|
||||||
|
v_project_id INT;
|
||||||
|
BEGIN
|
||||||
|
SELECT p.org_unit_id, ou.title, rpr.year, ou.utc_offset, rpr.report_type, rpr.project_id
|
||||||
|
INTO v_org_unit_id, v_org_unit_title, v_year, v_utc_offset, v_report_type, v_project_id
|
||||||
|
FROM v3.rf_project_report rpr
|
||||||
|
LEFT JOIN v3.project p ON p.id = rpr.project_id
|
||||||
|
LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id
|
||||||
|
WHERE rpr.id = p_rf_project_report_id;
|
||||||
|
|
||||||
|
INSERT INTO v3.form3_phase
|
||||||
|
(rf_project_report_id, phase_code, role, column_keys, opens_at, closes_at)
|
||||||
|
VALUES (
|
||||||
|
p_rf_project_report_id,
|
||||||
|
p_phase_code,
|
||||||
|
p_role,
|
||||||
|
p_column_keys,
|
||||||
|
CASE WHEN p_opens_at IS NOT NULL THEN (p_opens_at::text || v_utc_offset)::timestamptz ELSE NULL END,
|
||||||
|
CASE WHEN p_closes_at IS NOT NULL THEN (p_closes_at::text || v_utc_offset)::timestamptz ELSE NULL END
|
||||||
|
|
||||||
|
)
|
||||||
|
RETURNING * INTO v_phase;
|
||||||
|
|
||||||
|
|
||||||
|
v_task_name := trim(concat(
|
||||||
|
'FORM_3',
|
||||||
|
CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
|
||||||
|
CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
|
||||||
|
));
|
||||||
|
v_phase_id := format('%s:%s', p_rf_project_report_id, p_phase_code);
|
||||||
|
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
||||||
|
jsonb_build_object(
|
||||||
|
'entity_type', 'project',
|
||||||
|
'entity_id', v_project_id,
|
||||||
|
'core_entity_type', 'form3_phase',
|
||||||
|
'core_entity_id', v_phase_id,
|
||||||
|
'action', 'create',
|
||||||
|
'phase_id', v_phase_id,
|
||||||
|
'task_name', NULLIF(v_task_name, ''),
|
||||||
|
'report_id', p_rf_project_report_id,
|
||||||
|
'org_unit_id', v_org_unit_id,
|
||||||
|
'org_unit_name', v_org_unit_title,
|
||||||
|
'year', v_year,
|
||||||
|
'project_id', v_project_id,
|
||||||
|
'report_type', v_report_type,
|
||||||
|
'phase_code', p_phase_code,
|
||||||
|
'role', v_phase.role,
|
||||||
|
'column_keys', v_phase.column_keys,
|
||||||
|
'opens_at', v_phase.opens_at,
|
||||||
|
'closes_at', v_phase.closes_at
|
||||||
|
),
|
||||||
|
null, null, v_org_unit_id
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN v_phase;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION v3.upd_form3_phase(p_rf_project_report_id integer, p_phase_code character varying, p_role character varying DEFAULT NULL::character varying, p_column_keys text[] DEFAULT NULL::text[], p_opens_at timestamp without time zone DEFAULT NULL::timestamp without time zone, p_closes_at timestamp without time zone DEFAULT NULL::timestamp without time zone)
|
||||||
|
RETURNS v3.form3_phase
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_old v3.form3_phase;
|
||||||
|
v_new v3.form3_phase;
|
||||||
|
v_is_extend BOOLEAN := FALSE;
|
||||||
|
v_has_non_extend_change BOOLEAN := FALSE;
|
||||||
|
v_has_window_change BOOLEAN := FALSE;
|
||||||
|
v_changes JSONB := '{}'::jsonb;
|
||||||
|
v_org_unit_id INT;
|
||||||
|
v_org_unit_title VARCHAR;
|
||||||
|
v_form_type_code VARCHAR;
|
||||||
|
v_year INT;
|
||||||
|
v_task_name TEXT;
|
||||||
|
v_phase_id TEXT;
|
||||||
|
v_utc_offset VARCHAR(3);
|
||||||
|
v_opens_at timestamptz;
|
||||||
|
v_closes_at timestamptz;
|
||||||
|
v_report_type VARCHAR;
|
||||||
|
v_project_id INT;
|
||||||
|
BEGIN
|
||||||
|
SELECT * INTO v_old
|
||||||
|
FROM v3.form3_phase
|
||||||
|
WHERE rf_project_report_id = p_rf_project_report_id
|
||||||
|
AND phase_code = p_phase_code;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'form3_phase не найден (rf_project_report_id=%, phase=%)',
|
||||||
|
p_rf_project_report_id, p_phase_code;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT p.org_unit_id, ou.title, rpr.year, ou.utc_offset, rpr.report_type, rpr.project_id
|
||||||
|
INTO v_org_unit_id, v_org_unit_title, v_year, v_utc_offset, v_report_type, v_project_id
|
||||||
|
FROM v3.rf_project_report rpr
|
||||||
|
LEFT JOIN v3.project p ON p.id = rpr.project_id
|
||||||
|
LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id
|
||||||
|
WHERE rpr.id = p_rf_project_report_id;
|
||||||
|
v_task_name := trim(concat(
|
||||||
|
'FORM_3',
|
||||||
|
CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
|
||||||
|
CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
|
||||||
|
));
|
||||||
|
|
||||||
|
IF p_opens_at is not null THEN
|
||||||
|
v_opens_at = (p_opens_at::text || v_utc_offset)::timestamptz;
|
||||||
|
END IF;
|
||||||
|
IF p_closes_at is not null THEN
|
||||||
|
v_closes_at = (p_closes_at::text || v_utc_offset)::timestamptz;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
UPDATE v3.form3_phase
|
||||||
|
SET role = COALESCE(p_role, role),
|
||||||
|
column_keys = COALESCE(p_column_keys, column_keys),
|
||||||
|
opens_at = COALESCE(v_opens_at, opens_at),
|
||||||
|
closes_at = COALESCE(v_closes_at, closes_at)
|
||||||
|
WHERE rf_project_report_id = p_rf_project_report_id
|
||||||
|
AND phase_code = p_phase_code
|
||||||
|
RETURNING * INTO v_new;
|
||||||
|
|
||||||
|
|
||||||
|
v_phase_id := format('%s:%s', p_rf_project_report_id, p_phase_code);
|
||||||
|
|
||||||
|
v_is_extend := v_new.closes_at > v_old.closes_at;
|
||||||
|
v_has_non_extend_change := v_old.role IS DISTINCT FROM v_new.role
|
||||||
|
OR v_old.column_keys IS DISTINCT FROM v_new.column_keys
|
||||||
|
OR v_old.opens_at IS DISTINCT FROM v_new.opens_at;
|
||||||
|
v_has_window_change := v_has_non_extend_change
|
||||||
|
OR (v_old.closes_at IS DISTINCT FROM v_new.closes_at AND NOT v_is_extend);
|
||||||
|
|
||||||
|
IF v_old.role IS DISTINCT FROM v_new.role THEN
|
||||||
|
v_changes := v_changes || jsonb_build_object(
|
||||||
|
'role', jsonb_build_object('before', v_old.role, 'after', v_new.role)
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
IF v_old.column_keys IS DISTINCT FROM v_new.column_keys THEN
|
||||||
|
v_changes := v_changes || jsonb_build_object(
|
||||||
|
'column_keys', jsonb_build_object('before', v_old.column_keys, 'after', v_new.column_keys)
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
IF v_old.opens_at IS DISTINCT FROM v_new.opens_at THEN
|
||||||
|
v_changes := v_changes || jsonb_build_object(
|
||||||
|
'opens_at', jsonb_build_object(
|
||||||
|
'before', v_old.opens_at,
|
||||||
|
'after', v_new.opens_at
|
||||||
|
)
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
IF v_old.closes_at IS DISTINCT FROM v_new.closes_at THEN
|
||||||
|
v_changes := v_changes || jsonb_build_object(
|
||||||
|
'closes_at', jsonb_build_object(
|
||||||
|
'before', v_old.closes_at,
|
||||||
|
'after', v_new.closes_at
|
||||||
|
)
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_is_extend THEN
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'ACCESS_EXTEND', 'ACCESS',
|
||||||
|
jsonb_build_object(
|
||||||
|
'entity_type', 'project',
|
||||||
|
'entity_id', v_project_id,
|
||||||
|
'core_entity_type', 'form3_phase',
|
||||||
|
'core_entity_id', v_phase_id,
|
||||||
|
'action', 'update',
|
||||||
|
'phase_id', v_phase_id,
|
||||||
|
'phase_name', p_phase_code,
|
||||||
|
'task_name', NULLIF(v_task_name, ''),
|
||||||
|
'report_id', p_rf_project_report_id,
|
||||||
|
'report_type', v_report_type,
|
||||||
|
'project_id', v_project_id,
|
||||||
|
'org_unit_id', v_org_unit_id,
|
||||||
|
'org_unit_name', v_org_unit_title,
|
||||||
|
'year', v_year,
|
||||||
|
'phase_code', p_phase_code,
|
||||||
|
'closes_at_before', v_old.closes_at,
|
||||||
|
'closes_at_after', v_new.closes_at
|
||||||
|
),
|
||||||
|
null, null, v_org_unit_id
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_has_window_change THEN
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
||||||
|
jsonb_build_object(
|
||||||
|
'entity_type', 'project',
|
||||||
|
'entity_id', v_project_id,
|
||||||
|
'core_entity_type', 'form3_phase',
|
||||||
|
'core_entity_id', v_phase_id,
|
||||||
|
'action', 'update',
|
||||||
|
'phase_id', v_phase_id,
|
||||||
|
'phase_name', p_phase_code,
|
||||||
|
'task_id', p_rf_project_report_id,
|
||||||
|
'task_name', NULLIF(v_task_name, ''),
|
||||||
|
'project_id', v_project_id,
|
||||||
|
'report_type', v_report_type,
|
||||||
|
'year', v_year,
|
||||||
|
'org_unit_id', v_org_unit_id,
|
||||||
|
'org_unit_name', v_org_unit_title,
|
||||||
|
'phase_code', p_phase_code,
|
||||||
|
'changes', v_changes
|
||||||
|
),
|
||||||
|
null, null, v_org_unit_id
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN v_new;
|
||||||
|
END;
|
||||||
|
$function$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION v3.del_form3_phase(p_rf_project_report_id integer, p_phase_code character varying)
|
||||||
|
RETURNS boolean
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_old v3.form3_phase;
|
||||||
|
v_org_unit_id INT;
|
||||||
|
v_org_unit_title VARCHAR;
|
||||||
|
v_year INT;
|
||||||
|
v_task_name TEXT;
|
||||||
|
v_phase_id TEXT;
|
||||||
|
v_report_type VARCHAR;
|
||||||
|
v_project_id INT;
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM v3.form3_phase
|
||||||
|
WHERE rf_project_report_id = p_rf_project_report_id
|
||||||
|
AND phase_code = p_phase_code
|
||||||
|
RETURNING * INTO v_old;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RETURN FALSE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT p.org_unit_id, ou.title, rpr.year, rpr.report_type, rpr.project_id
|
||||||
|
INTO v_org_unit_id, v_org_unit_title, v_year, v_report_type, v_project_id
|
||||||
|
FROM v3.rf_project_report rpr
|
||||||
|
LEFT JOIN v3.project p ON p.id = rpr.project_id
|
||||||
|
LEFT JOIN v3.org_unit ou ON ou.id = p.org_unit_id
|
||||||
|
WHERE rpr.id = p_rf_project_report_id;
|
||||||
|
v_task_name := trim(concat(
|
||||||
|
'FORM_3',
|
||||||
|
CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
|
||||||
|
CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
|
||||||
|
));
|
||||||
|
v_phase_id := format('%s:%s', p_rf_project_report_id, p_phase_code);
|
||||||
|
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
||||||
|
jsonb_build_object(
|
||||||
|
'entity_type', 'project',
|
||||||
|
'entity_id', v_project_id,
|
||||||
|
'core_entity_type', 'form3_phase',
|
||||||
|
'core_entity_id', v_phase_id,
|
||||||
|
'action', 'delete',
|
||||||
|
'phase_id', v_phase_id,
|
||||||
|
'phase_name', p_phase_code,
|
||||||
|
'task_name', NULLIF(v_task_name, ''),
|
||||||
|
'project_id', v_project_id,
|
||||||
|
'year', v_year,
|
||||||
|
'report_type', v_report_type,
|
||||||
|
'org_unit_id', v_org_unit_id,
|
||||||
|
'org_unit_name', v_org_unit_title,
|
||||||
|
'phase_code', p_phase_code,
|
||||||
|
'role', v_old.role,
|
||||||
|
'column_keys', v_old.column_keys,
|
||||||
|
'opens_at', v_old.opens_at,
|
||||||
|
'closes_at', v_old.closes_at
|
||||||
|
),
|
||||||
|
null, null, v_org_unit_id
|
||||||
|
);
|
||||||
|
RETURN TRUE;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION v3.add_project(p_name character varying, p_year integer, p_org_unit_id integer, p_level character varying DEFAULT 'project'::character varying, p_parent_id integer DEFAULT NULL::integer, p_project_type character varying DEFAULT NULL::character varying, p_vsp_format character varying DEFAULT NULL::character varying, p_placement_type character varying DEFAULT NULL::character varying, p_object_address character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_system_code character varying DEFAULT NULL::character varying)
|
||||||
|
RETURNS TABLE(project_id integer, limit_report_id integer, current_expenses_report_id integer)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_pid INT;
|
||||||
|
v_lim INT;
|
||||||
|
v_cur INT;
|
||||||
|
BEGIN
|
||||||
|
IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
|
||||||
|
RAISE EXCEPTION 'p_name обязателен';
|
||||||
|
END IF;
|
||||||
|
IF p_year IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_year обязателен';
|
||||||
|
END IF;
|
||||||
|
IF p_org_unit_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_org_unit_id обязателен';
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.org_unit WHERE id = p_org_unit_id) THEN
|
||||||
|
RAISE EXCEPTION 'org_unit id=% не существует', p_org_unit_id;
|
||||||
|
END IF;
|
||||||
|
IF p_parent_id IS NOT NULL AND
|
||||||
|
NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_parent_id AND level = 'program') THEN
|
||||||
|
RAISE EXCEPTION 'parent project id=% не существует или не имеет level=program', p_parent_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- INSERT project (CHECK-ы name/project_type/vsp_format/placement_type
|
||||||
|
-- из migrate_form3_v1.sql применятся автоматически).
|
||||||
|
INSERT INTO v3.project (
|
||||||
|
name, level, parent_id, org_unit_id,
|
||||||
|
project_type, vsp_format, placement_type, object_address, staff_count, total_area,
|
||||||
|
system_code
|
||||||
|
) VALUES (
|
||||||
|
p_name, p_level, p_parent_id, p_org_unit_id,
|
||||||
|
p_project_type, p_vsp_format, p_placement_type, p_object_address, p_staff_count, p_total_area,
|
||||||
|
p_system_code
|
||||||
|
) RETURNING id INTO v_pid;
|
||||||
|
|
||||||
|
-- Первый год: 2 отчёта (LIMIT + CURRENT_EXPENSES) + snapshot фаз + сетка статей.
|
||||||
|
-- p_emit_audit=false — событие года покрыто PROJECT_CREATE ниже. Дальнейшие
|
||||||
|
-- годы добавляются отдельно через v3.add_project_year.
|
||||||
|
SELECT y.limit_report_id, y.current_expenses_report_id
|
||||||
|
INTO v_lim, v_cur
|
||||||
|
FROM v3.add_project_year(v_pid, p_year, false) y;
|
||||||
|
|
||||||
|
-- Аудит: PROJECT_CREATE — все заполненные поля + id отчётов
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'PROJECT_CREATE', 'PROJECT',
|
||||||
|
jsonb_strip_nulls(jsonb_build_object(
|
||||||
|
'project_id', v_pid,
|
||||||
|
'name', p_name,
|
||||||
|
'year', p_year,
|
||||||
|
'level', p_level,
|
||||||
|
'parent_id', p_parent_id,
|
||||||
|
'org_unit_id', p_org_unit_id,
|
||||||
|
'project_type', p_project_type,
|
||||||
|
'vsp_format', p_vsp_format,
|
||||||
|
'placement_type', p_placement_type,
|
||||||
|
'object_address', p_object_address,
|
||||||
|
'system_code', p_system_code,
|
||||||
|
'staff_count', p_staff_count,
|
||||||
|
'total_area', p_total_area,
|
||||||
|
'limit_report_id', v_lim,
|
||||||
|
'current_expenses_report_id', v_cur
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN QUERY SELECT v_pid, v_lim, v_cur;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION v3.upd_phases_timezones(p_org_unit_id integer, p_old_tz character varying, p_new_tz character varying)
|
||||||
|
RETURNS integer
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_result integer;
|
||||||
|
v_result3 integer;
|
||||||
|
BEGIN
|
||||||
|
IF p_old_tz = p_new_tz THEN
|
||||||
|
RETURN 0;
|
||||||
|
END IF;
|
||||||
|
WITH updated AS (
|
||||||
|
UPDATE v3.form_phase
|
||||||
|
SET opens_at = ((opens_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz,
|
||||||
|
closes_at = ((closes_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz
|
||||||
|
WHERE budget_form_id IN (SELECT id FROM v3.budget_form WHERE org_unit_id = p_org_unit_id)
|
||||||
|
AND (
|
||||||
|
opens_at >= CURRENT_TIMESTAMP
|
||||||
|
OR closes_at >= CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
RETURNING *
|
||||||
|
)
|
||||||
|
SELECT COUNT(*) INTO v_result FROM updated;
|
||||||
|
|
||||||
|
WITH updated AS (
|
||||||
|
UPDATE v3.form3_phase
|
||||||
|
SET opens_at = ((opens_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz,
|
||||||
|
closes_at = ((closes_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz
|
||||||
|
WHERE rf_project_report_id IN (
|
||||||
|
SELECT id FROM V3.rf_project_report rpr WHERE project_id IN (
|
||||||
|
SELECT id FROM v3.project WHERE org_unit_id = p_org_unit_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
opens_at >= CURRENT_TIMESTAMP
|
||||||
|
OR closes_at >= CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
RETURNING *
|
||||||
|
)
|
||||||
|
SELECT COUNT(*) INTO v_result3 FROM updated;
|
||||||
|
RETURN v_result + v_result3;
|
||||||
|
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION v3.add_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying, p_role character varying, p_column_keys text[], p_opens_at timestamp without time zone, p_closes_at timestamp without time zone)
|
||||||
|
RETURNS v3.form_phase
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_phase v3.form_phase;
|
||||||
|
v_org_unit_id INT;
|
||||||
|
v_org_unit_title VARCHAR;
|
||||||
|
v_form_type_code VARCHAR;
|
||||||
|
v_year INT;
|
||||||
|
v_task_name TEXT;
|
||||||
|
v_phase_id TEXT;
|
||||||
|
v_utc_offset VARCHAR(3);
|
||||||
|
BEGIN
|
||||||
|
SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year
|
||||||
|
INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year
|
||||||
|
FROM v3.budget_form bf
|
||||||
|
LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
|
||||||
|
WHERE bf.id = p_budget_form_id;
|
||||||
|
|
||||||
|
SELECT utc_offset INTO v_utc_offset
|
||||||
|
FROM v3.org_unit
|
||||||
|
WHERE id = v_org_unit_id;
|
||||||
|
|
||||||
|
INSERT INTO v3.form_phase(
|
||||||
|
budget_form_id, sheet, phase_code, role, column_keys, opens_at, closes_at
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
p_budget_form_id,
|
||||||
|
p_sheet,
|
||||||
|
p_phase_code,
|
||||||
|
p_role,
|
||||||
|
p_column_keys,
|
||||||
|
CASE WHEN p_opens_at IS NOT NULL THEN (p_opens_at::text || v_utc_offset)::timestamptz ELSE NULL END,
|
||||||
|
CASE WHEN p_closes_at IS NOT NULL THEN (p_closes_at::text || v_utc_offset)::timestamptz ELSE NULL END
|
||||||
|
|
||||||
|
)
|
||||||
|
RETURNING * INTO v_phase;
|
||||||
|
|
||||||
|
|
||||||
|
v_task_name := trim(concat(
|
||||||
|
COALESCE(v_form_type_code, ''),
|
||||||
|
CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
|
||||||
|
CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
|
||||||
|
));
|
||||||
|
v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code);
|
||||||
|
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
||||||
|
jsonb_build_object(
|
||||||
|
'entity_type', '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
|
||||||
84
api/alembic/versions/0010_form4.py
Normal file
84
api/alembic/versions/0010_form4.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision = "0010"
|
||||||
|
down_revision = "0009"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
_DOLLAR_TAG_RE = re.compile(r"\$\w+\$")
|
||||||
|
|
||||||
|
|
||||||
|
def _find_dollar_tag(line: str) -> str | None:
|
||||||
|
m = _DOLLAR_TAG_RE.search(line.strip())
|
||||||
|
return m.group(0) if m else None
|
||||||
|
|
||||||
|
|
||||||
|
def _split_statements(sql: str) -> list[str]:
|
||||||
|
statements: list[str] = []
|
||||||
|
current: list[str] = []
|
||||||
|
in_dollar = False
|
||||||
|
dollar_tag: str | None = None
|
||||||
|
|
||||||
|
for line in sql.split("\n"):
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith("--"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not in_dollar:
|
||||||
|
tag = _find_dollar_tag(stripped)
|
||||||
|
if tag and tag.endswith("$") and tag.startswith("$"):
|
||||||
|
dollar_tag = tag
|
||||||
|
in_dollar = True
|
||||||
|
current.append(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_dollar and dollar_tag and stripped.startswith(dollar_tag):
|
||||||
|
after = stripped[len(dollar_tag):].strip()
|
||||||
|
if after == ";" or after == "":
|
||||||
|
in_dollar = False
|
||||||
|
dollar_tag = None
|
||||||
|
if after == ";":
|
||||||
|
current.append(line)
|
||||||
|
statements.append("\n".join(current))
|
||||||
|
current = []
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not in_dollar and stripped.rstrip().endswith(";"):
|
||||||
|
current.append(line)
|
||||||
|
statements.append("\n".join(current))
|
||||||
|
current = []
|
||||||
|
continue
|
||||||
|
|
||||||
|
current.append(line)
|
||||||
|
|
||||||
|
remaining = "\n".join(current).strip()
|
||||||
|
if remaining:
|
||||||
|
statements.append(remaining)
|
||||||
|
|
||||||
|
return statements
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
ddl_path = os.path.join(os.path.dirname(__file__), "sql", "0010_form4.sql")
|
||||||
|
with open(ddl_path) as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
statements = _split_statements(content)
|
||||||
|
for stmt in statements:
|
||||||
|
stripped = stmt.strip().rstrip(";").strip()
|
||||||
|
if not stripped:
|
||||||
|
continue
|
||||||
|
if all(l.strip().startswith("--") or not l.strip() for l in stripped.split("\n")):
|
||||||
|
continue
|
||||||
|
op.execute(stripped)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
0
api/alembic/versions/__init__.py
Normal file
0
api/alembic/versions/__init__.py
Normal file
@ -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, int4, varchar, date);
|
-- DROP FUNCTION v3.add_vsp(int4, varchar, varchar, varchar, date, varchar, int4, numeric, date, bool, bool, varchar, int4, varchar, text, 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_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)
|
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)
|
||||||
RETURNS integer
|
RETURNS integer
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
AS $function$
|
AS $function$
|
||||||
@ -1153,14 +1153,12 @@ 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, location_form, numbers,
|
vsp_type, notes, rent_contract_num, rent_end_date)
|
||||||
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_location_form, p_numbers,
|
p_vsp_type, p_notes, p_rent_contract_num, p_rent_end_date)
|
||||||
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(
|
||||||
@ -1184,8 +1182,6 @@ 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,
|
||||||
@ -1574,7 +1570,7 @@ AS $function$
|
|||||||
DECLARE
|
DECLARE
|
||||||
v_report_id INT;
|
v_report_id INT;
|
||||||
v_eid INT;
|
v_eid INT;
|
||||||
v_org_id INT;
|
v_form_id INT;
|
||||||
v_ei_path INT[];
|
v_ei_path INT[];
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Извлекаем report_id + ei ДО DELETE (нужен для path)
|
-- Извлекаем report_id + ei ДО DELETE (нужен для path)
|
||||||
@ -1588,21 +1584,33 @@ 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,
|
||||||
'entity_id', p_line_id,
|
'form_id', v_form_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 уже нет)
|
||||||
@ -2738,9 +2746,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, int4, varchar, date);
|
-- DROP FUNCTION v3.upd_vsp(int4, int4, varchar, varchar, varchar, date, varchar, int4, numeric, date, bool, bool, varchar, int4, varchar, text, 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_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)
|
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)
|
||||||
RETURNS void
|
RETURNS void
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
AS $function$
|
AS $function$
|
||||||
@ -2758,8 +2766,7 @@ 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.location_form, v.numbers,
|
v.vsp_type, v.notes, v.rent_contract_num, v.rent_end_date
|
||||||
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;
|
||||||
@ -2892,20 +2899,6 @@ 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',
|
||||||
@ -4093,15 +4086,13 @@ 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_s INT; v_ahr_d INT;
|
v_ahr INT; v_cap INT; v_oper 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
|
||||||
-- Resolve form_ids: within each (year, ssp, sheet), lower bf.id = Support, higher = Development
|
-- Одна budget_form на лист содержит ОБА направления (budget_line.direction).
|
||||||
|
-- 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
|
||||||
@ -4109,18 +4100,14 @@ BEGIN
|
|||||||
GROUP BY bf.id, ei.sheet
|
GROUP BY bf.id, ei.sheet
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
MAX(CASE WHEN sh='AHR' AND rn=1 THEN fid END),
|
MAX(CASE WHEN sh='AHR' THEN fid END),
|
||||||
MAX(CASE WHEN sh='AHR' AND rn=2 THEN fid END),
|
MAX(CASE WHEN sh='CAP' THEN fid END),
|
||||||
MAX(CASE WHEN sh='CAP' AND rn=1 THEN fid END),
|
MAX(CASE WHEN sh='OPER' THEN fid END)
|
||||||
MAX(CASE WHEN sh='CAP' AND rn=2 THEN fid END),
|
INTO v_ahr, v_cap, v_oper
|
||||||
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,
|
||||||
@ -4128,7 +4115,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_s, 'AHR', 'Support', v_sections)
|
FROM v_form1_sheet_sections(v_ahr, 'AHR', 'Support', v_sections)
|
||||||
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
||||||
),
|
),
|
||||||
ahr_d AS (
|
ahr_d AS (
|
||||||
@ -4138,7 +4125,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_d, 'AHR', 'Development', v_sections)
|
FROM v_form1_sheet_sections(v_ahr, 'AHR', 'Development', v_sections)
|
||||||
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
||||||
),
|
),
|
||||||
cap_s AS (
|
cap_s AS (
|
||||||
@ -4148,7 +4135,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_s, 'CAP', 'Support', v_sections)
|
FROM v_form1_sheet_sections(v_cap, 'CAP', 'Support', v_sections)
|
||||||
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
||||||
),
|
),
|
||||||
cap_d AS (
|
cap_d AS (
|
||||||
@ -4158,7 +4145,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_d, 'CAP', 'Development', v_sections)
|
FROM v_form1_sheet_sections(v_cap, 'CAP', 'Development', v_sections)
|
||||||
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
||||||
),
|
),
|
||||||
oper_s AS (
|
oper_s AS (
|
||||||
@ -4168,12 +4155,10 @@ 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_s, 'OPER', 'Support', v_sections)
|
FROM v_form1_sheet_sections(v_oper, '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,
|
||||||
@ -4188,7 +4173,6 @@ 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,
|
||||||
@ -4201,7 +4185,6 @@ 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,
|
||||||
@ -4232,7 +4215,6 @@ 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[])
|
||||||
@ -4427,7 +4409,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 and (p_direction is null OR bl.direction is null or bl.direction = p_direction)
|
WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet
|
||||||
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
|
||||||
),
|
),
|
||||||
@ -4453,7 +4435,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 and (p_direction is null OR bl.direction is null or bl.direction = p_direction)
|
WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet
|
||||||
GROUP BY bl.expense_item_id, q.quarter
|
GROUP BY bl.expense_item_id, q.quarter
|
||||||
),
|
),
|
||||||
-- Tree-rollup
|
-- Tree-rollup
|
||||||
@ -7791,3 +7773,406 @@ 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,3 +4251,28 @@ 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;
|
||||||
2602
api/alembic/versions/sql/0006_form3.sql
Normal file
2602
api/alembic/versions/sql/0006_form3.sql
Normal file
File diff suppressed because it is too large
Load Diff
852
api/alembic/versions/sql/0010_form4.sql
Normal file
852
api/alembic/versions/sql/0010_form4.sql
Normal file
@ -0,0 +1,852 @@
|
|||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- migrate_form4_project.sql — выделение FORM_4 программ/проектов в отдельную
|
||||||
|
-- сущность v3.form4_project (перестают делить таблицу с FORM_3 v3.project).
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Мотивация: v3.project исторически был FORM_4-таблицей (program/project +
|
||||||
|
-- parent_id), FORM_3-редизайн навесил сверху свои атрибуты (project_type,
|
||||||
|
-- vsp_format, address, area, staff, org_unit_id) и строгий chk_v3_project_name.
|
||||||
|
-- Два разных концепта под одним словом «project» → разреженные колонки, нет
|
||||||
|
-- защиты FK, констрейнт вынужден ветвиться по «виду» строки. Разводим.
|
||||||
|
--
|
||||||
|
-- Новая таблица v3.form4_project — точный костяк без FORM_3-полей:
|
||||||
|
-- program (level='program', parent_id=NULL)
|
||||||
|
-- project (level='project', parent_id=program)
|
||||||
|
--
|
||||||
|
-- Дискриминатор FORM_4 vs FORM_3: FORM_3-строки — те, что ссылаются из
|
||||||
|
-- v3.rf_project_report (add_project всегда создаёт отчёты для КАЖДОЙ строки,
|
||||||
|
-- включая программы). Всё остальное в v3.project — FORM_4.
|
||||||
|
--
|
||||||
|
-- Порядок: после init.sql + всех migrate_form3*/migrate_to_identity (нужна
|
||||||
|
-- финальная форма v3.project с org_unit_id). Один прогон — copy защищён
|
||||||
|
-- проверкой на пустоту form4_project.
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- ───── 1. Новая таблица ─────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS v3.form4_project (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR NOT NULL,
|
||||||
|
level VARCHAR CHECK (level IN ('program', 'project')),
|
||||||
|
section_code VARCHAR,
|
||||||
|
parent_id INTEGER REFERENCES v3.form4_project(id),
|
||||||
|
-- org_unit_id INTEGER REFERENCES v3.org_unit(id),
|
||||||
|
form_id INTEGER REFERENCES v3.budget_form(id),
|
||||||
|
-- project (level='project') обязан иметь родителя-программу; program — нет.
|
||||||
|
CONSTRAINT chk_form4_project_parent CHECK (
|
||||||
|
(level = 'program' AND parent_id IS NULL)
|
||||||
|
OR (level = 'project')
|
||||||
|
),
|
||||||
|
CONSTRAINT chk_form4_project_section_code CHECK (
|
||||||
|
(level = 'program') = (section_code IS NOT NULL)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX ix_form4_project_section_code
|
||||||
|
ON v3.form4_project(section_code) WHERE level = 'program';
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_v3_form4_project_parent ON v3.form4_project(parent_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_v3_form4_project_form ON v3.form4_project(form_id);
|
||||||
|
|
||||||
|
-- ───── 2. Перенос FORM_4-строк из v3.project (с сохранением id) ──────────────
|
||||||
|
-- FK budget_line.project_id ссылается по id → id сохраняем 1:1.
|
||||||
|
-- Копируем обе роли (program + project) одним INSERT: self-FK parent_id внутри
|
||||||
|
-- одного оператора проверяется в конце, порядок строк не важен.
|
||||||
|
-- DO $migrate$
|
||||||
|
-- BEGIN
|
||||||
|
-- IF NOT EXISTS (SELECT 1 FROM v3.form4_project) THEN
|
||||||
|
-- INSERT INTO v3.form4_project (id, name, level, parent_id, org_unit_id)
|
||||||
|
-- SELECT p.id, p.name, p.level, p.parent_id, (p.org_unit_id
|
||||||
|
-- FROM v3.project p
|
||||||
|
-- WHERE p.id NOT IN (
|
||||||
|
-- SELECT project_id FROM v3.rf_project_report WHERE project_id IS NOT NULL
|
||||||
|
-- );
|
||||||
|
|
||||||
|
-- -- Синхронизируем sequence, чтобы новые INSERT не конфликтовали по id.
|
||||||
|
-- PERFORM setval('v3.form4_project_id_seq',
|
||||||
|
-- GREATEST((SELECT COALESCE(MAX(id), 0) FROM v3.form4_project), 1));
|
||||||
|
|
||||||
|
-- RAISE NOTICE 'form4_project: перенесено % строк из v3.project',
|
||||||
|
-- (SELECT count(*) FROM v3.form4_project);
|
||||||
|
-- ELSE
|
||||||
|
-- RAISE NOTICE 'form4_project уже наполнена — перенос пропущен';
|
||||||
|
-- END IF;
|
||||||
|
-- END
|
||||||
|
-- $migrate$;
|
||||||
|
|
||||||
|
-- ───── 3. Перецепка FK budget_line.project_id на новую таблицу ──────────────
|
||||||
|
ALTER TABLE v3.budget_line DROP CONSTRAINT IF EXISTS budget_line_project_id_fkey;
|
||||||
|
ALTER TABLE v3.budget_line ADD CONSTRAINT budget_line_project_id_fkey FOREIGN KEY (project_id) REFERENCES v3.form4_project(id);
|
||||||
|
|
||||||
|
-- ───── 4. Удаление перенесённых строк из v3.project ─────────────────────────
|
||||||
|
-- После перецепки budget_line на v3.project больше не ссылается ничего FORM_4.
|
||||||
|
-- Остаются только FORM_3-строки (referenced by rf_project_report).
|
||||||
|
DELETE FROM v3.project p
|
||||||
|
WHERE p.id NOT IN (
|
||||||
|
SELECT project_id FROM v3.rf_project_report WHERE project_id IS NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ───── 5. Восстановление chk_v3_project_name в строгой форме ─────────────────
|
||||||
|
-- FORM_4-строки ушли — v3.project теперь чисто FORM_3, правило коротких имён
|
||||||
|
-- ВСП применимо ко всей таблице (как до form4_write.sql). NOT VALID сохраняем.
|
||||||
|
ALTER TABLE v3.project DROP CONSTRAINT IF EXISTS chk_v3_project_name;
|
||||||
|
ALTER TABLE v3.project
|
||||||
|
ADD CONSTRAINT chk_v3_project_name
|
||||||
|
CHECK (name IS NULL OR (
|
||||||
|
length(name) <= 30 AND
|
||||||
|
name !~ E'[+\\-/\\\\=&* ]'
|
||||||
|
)) NOT VALID;
|
||||||
|
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- v3.add_form4_program / v3.add_form4_project — создание программ и проектов
|
||||||
|
-- FORM_4 в выделенной таблице v3.form4_project (иерархия program → project).
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- FORM_4 хранит иерархию отдельно от FORM_3 (v3.project) — см.
|
||||||
|
-- migrate_form4_project.sql. Костяк:
|
||||||
|
-- program (level='program', parent_id=NULL) → depth=1 (GROUP) в v_form4
|
||||||
|
-- project (level='project', parent_id=program) → depth=2 (ITEM) в v_form4
|
||||||
|
-- Строки бюджета привязываются к проекту через budget_line.project_id.
|
||||||
|
--
|
||||||
|
-- Никаких FORM_3-атрибутов / rf_project_report / snapshot фаз — это ветка
|
||||||
|
-- budget_form. p_org_unit_id опционален (read-path FORM_4 по нему не фильтрует,
|
||||||
|
-- заполняется для аудита/принадлежности).
|
||||||
|
--
|
||||||
|
-- Порядок применения: после migrate_form4_project.sql (нужна таблица
|
||||||
|
-- v3.form4_project) и audit.sql (нужна v3.log_event).
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- ───── 1. add_form4_program ─────────────────────────────────────────────────
|
||||||
|
DROP FUNCTION IF EXISTS v3.add_form4_program(VARCHAR, INT, VARCHAR, INT);
|
||||||
|
|
||||||
|
CREATE FUNCTION v3.add_form4_program(
|
||||||
|
p_name VARCHAR,
|
||||||
|
p_form_id INT,
|
||||||
|
p_section_code VARCHAR DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
project_id INT,
|
||||||
|
row_type VARCHAR,
|
||||||
|
depth INT,
|
||||||
|
sort_order BIGINT,
|
||||||
|
data JSONB
|
||||||
|
) AS $function$
|
||||||
|
#variable_conflict use_column
|
||||||
|
DECLARE
|
||||||
|
v_pid INT;
|
||||||
|
BEGIN
|
||||||
|
IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
|
||||||
|
RAISE EXCEPTION 'p_name обязателен';
|
||||||
|
END IF;
|
||||||
|
IF p_form_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_form_id обязателен';
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM v3.budget_form bf
|
||||||
|
WHERE bf.form_type_code = 'FORM_4' AND bf.id = p_form_id
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION 'Только FORM_4';
|
||||||
|
END IF;
|
||||||
|
IF p_section_code IS NOT NULL AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM v3.expense_item
|
||||||
|
WHERE section_code = p_section_code AND depth = 0
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION 'section_code=% не существует в expense_item (depth=0)', p_section_code;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
INSERT INTO v3.form4_project (name, level, parent_id, form_id, section_code)
|
||||||
|
VALUES (p_name, 'program', NULL, p_form_id, p_section_code)
|
||||||
|
RETURNING id INTO v_pid;
|
||||||
|
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'PROJECT_CREATE', 'PROJECT',
|
||||||
|
jsonb_strip_nulls(jsonb_build_object(
|
||||||
|
'project_id', v_pid,
|
||||||
|
'name', p_name,
|
||||||
|
'level', 'program',
|
||||||
|
'form_id', p_form_id,
|
||||||
|
'form_type', 'FORM_4'
|
||||||
|
)),
|
||||||
|
p_form_id, NULL, NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT v_pid, v.row_type, v.depth, v.sort_order, v.data
|
||||||
|
FROM v3.v_form4_sheet_jsonb(p_form_id, p_section_code, NULL) v
|
||||||
|
WHERE (v.row_type = 'GROUP' AND (v.data->'header'->>'program_id')::INT = v_pid)
|
||||||
|
OR (v.row_type = 'ROOT' AND v.data->'header'->>'section_code' = p_section_code)
|
||||||
|
ORDER BY v.sort_order;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql VOLATILE;
|
||||||
|
|
||||||
|
-- ───── 2. add_form4_project ─────────────────────────────────────────────────
|
||||||
|
DROP FUNCTION IF EXISTS v3.add_form4_project(VARCHAR, INT);
|
||||||
|
|
||||||
|
CREATE FUNCTION v3.add_form4_project(
|
||||||
|
p_name VARCHAR,
|
||||||
|
p_program_id INT
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
project_id INT,
|
||||||
|
row_type VARCHAR,
|
||||||
|
depth INT,
|
||||||
|
sort_order BIGINT,
|
||||||
|
data JSONB
|
||||||
|
)
|
||||||
|
AS $function$
|
||||||
|
#variable_conflict use_column
|
||||||
|
DECLARE
|
||||||
|
v_pid INT;
|
||||||
|
v_section_code VARCHAR;
|
||||||
|
v_form_id INT;
|
||||||
|
BEGIN
|
||||||
|
IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
|
||||||
|
RAISE EXCEPTION 'p_name обязателен';
|
||||||
|
END IF;
|
||||||
|
IF p_program_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_program_id обязателен (проект создаётся под программой)';
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.form4_project fp
|
||||||
|
WHERE fp.id = p_program_id AND fp.level = 'program') THEN
|
||||||
|
RAISE EXCEPTION 'program id=% не существует или не имеет level=program', p_program_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT form_id INTO v_form_id FROM v3.form4_project WHERE id = p_program_id;
|
||||||
|
|
||||||
|
INSERT INTO v3.form4_project (name, level, parent_id, form_id)
|
||||||
|
VALUES (p_name, 'project', p_program_id, v_form_id)
|
||||||
|
RETURNING id INTO v_pid;
|
||||||
|
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'PROJECT_CREATE', 'PROJECT',
|
||||||
|
jsonb_strip_nulls(jsonb_build_object(
|
||||||
|
'project_id', v_pid,
|
||||||
|
'name', p_name,
|
||||||
|
'level', 'project',
|
||||||
|
'parent_id', p_program_id,
|
||||||
|
'form_id', v_form_id,
|
||||||
|
'form_type', 'FORM_4'
|
||||||
|
)),
|
||||||
|
v_form_id, NULL, NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT fp.section_code INTO v_section_code
|
||||||
|
FROM v3.form4_project fp
|
||||||
|
WHERE fp.id = p_program_id AND fp.level = 'program';
|
||||||
|
|
||||||
|
IF v_section_code IS NOT NULL THEN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT v_pid, v.row_type, v.depth, v.sort_order, v.data
|
||||||
|
FROM v3.v_form4_sheet_jsonb(v_form_id, v_section_code, NULL) v
|
||||||
|
WHERE (v.row_type = 'ITEM' AND (v.data->'header'->>'project_id')::INT = v_pid)
|
||||||
|
OR (v.row_type = 'GROUP' AND (v.data->'header'->>'program_id')::INT = p_program_id)
|
||||||
|
OR (v.row_type = 'ROOT' AND v.data->'header'->>'section_code' = v_section_code)
|
||||||
|
ORDER BY v.sort_order;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql VOLATILE;
|
||||||
|
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- v3.v_form4_sheet_jsonb — JSONB-выдача FORM_4 для АХР_Р / КВ_Р / Операц
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Иерархия (4 уровня):
|
||||||
|
-- ROOT depth=0 — section_code (1.00.0/3.00.0/4.00.0/5.00.0)
|
||||||
|
-- PROGRAM depth=1 — v3.form4_project level=program
|
||||||
|
-- PROJECT depth=2 — v3.form4_project level=project (parent_id=program)
|
||||||
|
-- INPUT depth=3 — budget_line (R-код)
|
||||||
|
--
|
||||||
|
-- Финансовые блоки (plan, seq_dfip, approved, booking, q1-q4, totals)
|
||||||
|
-- агрегируются на уровнях ROOT/PROGRAM/PROJECT через v3.jsonb_numeric_sum.
|
||||||
|
-- Текстовые поля (counterparty, comment, justification…) на агрегатных
|
||||||
|
-- уровнях NULL'ятся (jsonb_numeric_sum их игнорирует).
|
||||||
|
--
|
||||||
|
-- p_form_id — id формы FORM_4
|
||||||
|
-- p_sheet — 'AHR' / 'CAP' / 'OPER'
|
||||||
|
-- p_sections — массив включаемых блоков (как в v_form4_sheet_lines_jsonb)
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- ───── 1. Хелпер: рекурсивная сумма численных листьев двух JSONB ──────────
|
||||||
|
CREATE OR REPLACE FUNCTION v3.jsonb_numeric_sum(a JSONB, b JSONB)
|
||||||
|
RETURNS JSONB LANGUAGE sql IMMUTABLE AS $function$
|
||||||
|
SELECT CASE
|
||||||
|
WHEN a IS NULL THEN b
|
||||||
|
WHEN b IS NULL THEN a
|
||||||
|
WHEN jsonb_typeof(a) = 'number' AND jsonb_typeof(b) = 'number' THEN
|
||||||
|
to_jsonb((a)::text::numeric + (b)::text::numeric)
|
||||||
|
WHEN jsonb_typeof(a) = 'object' AND jsonb_typeof(b) = 'object' THEN (
|
||||||
|
SELECT jsonb_object_agg(k,
|
||||||
|
v3.jsonb_numeric_sum(a -> k, b -> k)
|
||||||
|
)
|
||||||
|
FROM (
|
||||||
|
SELECT k FROM jsonb_object_keys(a) k
|
||||||
|
UNION
|
||||||
|
SELECT k FROM jsonb_object_keys(b) k
|
||||||
|
) keys
|
||||||
|
)
|
||||||
|
-- одна сторона число/объект, другая нет → берём числовую/объектную
|
||||||
|
WHEN jsonb_typeof(a) IN ('number','object') THEN a
|
||||||
|
WHEN jsonb_typeof(b) IN ('number','object') THEN b
|
||||||
|
ELSE NULL
|
||||||
|
END;
|
||||||
|
$function$;
|
||||||
|
|
||||||
|
DROP AGGREGATE IF EXISTS v3.jsonb_sum(JSONB);
|
||||||
|
CREATE AGGREGATE v3.jsonb_sum(JSONB) (
|
||||||
|
SFUNC = v3.jsonb_numeric_sum,
|
||||||
|
STYPE = JSONB
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ───── 2. Основная функция ─────────────────────────────────────────────────
|
||||||
|
CREATE OR REPLACE FUNCTION v3.v_form4_sheet_jsonb(
|
||||||
|
p_form_id INT,
|
||||||
|
p_sheet VARCHAR,
|
||||||
|
p_sections TEXT[] DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
row_type VARCHAR,
|
||||||
|
depth INT,
|
||||||
|
sort_order BIGINT,
|
||||||
|
data JSONB
|
||||||
|
)
|
||||||
|
AS $function$
|
||||||
|
-- ═══ 1. Базовые INPUT-строки с расширенной иерархией ═══════════════════════
|
||||||
|
WITH inp_raw AS (
|
||||||
|
SELECT j.*
|
||||||
|
FROM v3.v_form4_sheet_lines_jsonb(p_form_id, p_sheet, p_sections) j
|
||||||
|
WHERE j.row_type = 'INPUT'
|
||||||
|
),
|
||||||
|
inp AS (
|
||||||
|
SELECT
|
||||||
|
i.*,
|
||||||
|
bl.project_id AS line_project_id,
|
||||||
|
|
||||||
|
-- --- разрешение проекта ---
|
||||||
|
-- A. bl.project_id → project (level='project')
|
||||||
|
-- B. bl.project_id → program (level='program') — тогда проекта нет
|
||||||
|
CASE WHEN prj.id IS NOT NULL THEN prj.id END AS prj_id,
|
||||||
|
CASE WHEN prj.id IS NOT NULL THEN prj.name END AS prj_name,
|
||||||
|
|
||||||
|
-- --- разрешение программы ---
|
||||||
|
-- A. через проект: prog_via_prj
|
||||||
|
-- B. напрямую: prog_direct
|
||||||
|
COALESCE(prog_via_prj.id, prog_direct.id) AS prog_id,
|
||||||
|
COALESCE(prog_via_prj.name, prog_direct.name) AS prog_name,
|
||||||
|
|
||||||
|
-- --- section (depth=0) ---
|
||||||
|
sec.id AS sec_id,
|
||||||
|
sec.section_code AS sec_code,
|
||||||
|
sec.name AS sec_name
|
||||||
|
|
||||||
|
FROM inp_raw i
|
||||||
|
JOIN v3.budget_line bl ON bl.id = i.line_id
|
||||||
|
|
||||||
|
-- проект (level='project')
|
||||||
|
LEFT JOIN v3.form4_project prj
|
||||||
|
ON prj.id = bl.project_id AND prj.level = 'project'
|
||||||
|
|
||||||
|
-- программа через проект
|
||||||
|
LEFT JOIN v3.form4_project prog_via_prj
|
||||||
|
ON prog_via_prj.id = prj.parent_id AND prog_via_prj.level = 'program'
|
||||||
|
|
||||||
|
-- программа напрямую (bl.project_id → program)
|
||||||
|
LEFT JOIN v3.form4_project prog_direct
|
||||||
|
ON prog_direct.id = bl.project_id AND prog_direct.level = 'program'
|
||||||
|
|
||||||
|
-- раздел depth=0 (через expense_item)
|
||||||
|
JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
WITH RECURSIVE up AS (
|
||||||
|
SELECT id, parent_id, name, section_code, depth
|
||||||
|
FROM v3.expense_item WHERE id = ei.id
|
||||||
|
UNION ALL
|
||||||
|
SELECT e.id, e.parent_id, e.name, e.section_code, e.depth
|
||||||
|
FROM v3.expense_item e JOIN up u ON u.parent_id = e.id
|
||||||
|
)
|
||||||
|
SELECT id, name, section_code FROM up WHERE depth = 0 LIMIT 1
|
||||||
|
) sec ON TRUE
|
||||||
|
),
|
||||||
|
|
||||||
|
-- ═══ 2. Все программы (включая пустые) ═════════════════════════════════════
|
||||||
|
all_programs AS (
|
||||||
|
SELECT
|
||||||
|
fp.id AS prog_id,
|
||||||
|
fp.name AS prog_name,
|
||||||
|
fp.section_code,
|
||||||
|
sec.id AS sec_id,
|
||||||
|
sec.section_code AS sec_code,
|
||||||
|
sec.name AS sec_name
|
||||||
|
FROM v3.form4_project fp
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT id, section_code, name
|
||||||
|
FROM v3.expense_item
|
||||||
|
WHERE section_code = fp.section_code AND depth = 0
|
||||||
|
LIMIT 1
|
||||||
|
) sec ON TRUE
|
||||||
|
WHERE fp.level = 'program'
|
||||||
|
AND (p_sections IS NULL OR fp.section_code = ANY(p_sections))
|
||||||
|
AND (fp.section_code = p_sheet) AND fp.form_id = p_form_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- ═══ 3. Все проекты под программами (включая пустые) ═══════════════════════
|
||||||
|
all_projects AS (
|
||||||
|
SELECT
|
||||||
|
prj.id AS prj_id,
|
||||||
|
prj.name AS prj_name,
|
||||||
|
prj.parent_id AS prog_id,
|
||||||
|
ap.sec_id, ap.sec_code, ap.sec_name
|
||||||
|
FROM v3.form4_project prj
|
||||||
|
JOIN all_programs ap ON ap.prog_id = prj.parent_id
|
||||||
|
WHERE prj.level = 'project' AND (ap.section_code = p_sheet)
|
||||||
|
),
|
||||||
|
|
||||||
|
-- ═══ 4. Агрегаты с финансовыми данными (только из inp) ════════════════════
|
||||||
|
|
||||||
|
-- PROJECT-агрегаты
|
||||||
|
prj_agg AS (
|
||||||
|
SELECT
|
||||||
|
sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name,
|
||||||
|
prog_id, MAX(prog_name) AS prog_name,
|
||||||
|
prj_id, MAX(prj_name) AS prj_name,
|
||||||
|
v3.jsonb_sum(plan_data) AS plan_data,
|
||||||
|
v3.jsonb_sum(seq_dfip_data) AS seq_dfip_data,
|
||||||
|
v3.jsonb_sum(approved_data) AS approved_data,
|
||||||
|
v3.jsonb_sum(contract_summary_data) AS contract_summary_data,
|
||||||
|
v3.jsonb_sum(allocation_data) AS allocation_data,
|
||||||
|
v3.jsonb_sum(reserve_data) AS reserve_data,
|
||||||
|
v3.jsonb_sum(collegial_data) AS collegial_data,
|
||||||
|
v3.jsonb_sum(ckk_data) AS ckk_data,
|
||||||
|
v3.jsonb_sum(contract_data) AS contract_data,
|
||||||
|
v3.jsonb_sum(booking_data) AS booking_data,
|
||||||
|
v3.jsonb_sum(q1_data) AS q1_data,
|
||||||
|
v3.jsonb_sum(q2_data) AS q2_data,
|
||||||
|
v3.jsonb_sum(q3_data) AS q3_data,
|
||||||
|
v3.jsonb_sum(q4_data) AS q4_data,
|
||||||
|
v3.jsonb_sum(totals_data) AS totals_data
|
||||||
|
FROM inp WHERE prj_id IS NOT NULL
|
||||||
|
GROUP BY sec_id, prog_id, prj_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- PROGRAM-агрегаты
|
||||||
|
prog_agg AS (
|
||||||
|
SELECT
|
||||||
|
sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name,
|
||||||
|
prog_id, MAX(prog_name) AS prog_name,
|
||||||
|
v3.jsonb_sum(plan_data) AS plan_data,
|
||||||
|
v3.jsonb_sum(seq_dfip_data) AS seq_dfip_data,
|
||||||
|
v3.jsonb_sum(approved_data) AS approved_data,
|
||||||
|
v3.jsonb_sum(contract_summary_data) AS contract_summary_data,
|
||||||
|
v3.jsonb_sum(allocation_data) AS allocation_data,
|
||||||
|
v3.jsonb_sum(reserve_data) AS reserve_data,
|
||||||
|
v3.jsonb_sum(collegial_data) AS collegial_data,
|
||||||
|
v3.jsonb_sum(ckk_data) AS ckk_data,
|
||||||
|
v3.jsonb_sum(contract_data) AS contract_data,
|
||||||
|
v3.jsonb_sum(booking_data) AS booking_data,
|
||||||
|
v3.jsonb_sum(q1_data) AS q1_data,
|
||||||
|
v3.jsonb_sum(q2_data) AS q2_data,
|
||||||
|
v3.jsonb_sum(q3_data) AS q3_data,
|
||||||
|
v3.jsonb_sum(q4_data) AS q4_data,
|
||||||
|
v3.jsonb_sum(totals_data) AS totals_data
|
||||||
|
FROM inp WHERE prog_id IS NOT NULL
|
||||||
|
GROUP BY sec_id, prog_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- ROOT-агрегаты
|
||||||
|
root_agg AS (
|
||||||
|
SELECT
|
||||||
|
sec_id, MAX(sec_code) AS sec_code, MAX(sec_name) AS sec_name,
|
||||||
|
v3.jsonb_sum(plan_data) AS plan_data,
|
||||||
|
v3.jsonb_sum(seq_dfip_data) AS seq_dfip_data,
|
||||||
|
v3.jsonb_sum(approved_data) AS approved_data,
|
||||||
|
v3.jsonb_sum(contract_summary_data) AS contract_summary_data,
|
||||||
|
v3.jsonb_sum(allocation_data) AS allocation_data,
|
||||||
|
v3.jsonb_sum(reserve_data) AS reserve_data,
|
||||||
|
v3.jsonb_sum(collegial_data) AS collegial_data,
|
||||||
|
v3.jsonb_sum(ckk_data) AS ckk_data,
|
||||||
|
v3.jsonb_sum(contract_data) AS contract_data,
|
||||||
|
v3.jsonb_sum(booking_data) AS booking_data,
|
||||||
|
v3.jsonb_sum(q1_data) AS q1_data,
|
||||||
|
v3.jsonb_sum(q2_data) AS q2_data,
|
||||||
|
v3.jsonb_sum(q3_data) AS q3_data,
|
||||||
|
v3.jsonb_sum(q4_data) AS q4_data,
|
||||||
|
v3.jsonb_sum(totals_data) AS totals_data
|
||||||
|
FROM inp
|
||||||
|
GROUP BY sec_id
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Нулевой JSONB-шаблон (для пустых программ/проектов)
|
||||||
|
zero AS (
|
||||||
|
SELECT '{}'::jsonb AS z
|
||||||
|
),
|
||||||
|
|
||||||
|
-- ═══ 5. UNION всех уровней ═════════════════════════════════════════════════
|
||||||
|
unioned AS (
|
||||||
|
-- ROOT — только из агрегатов (не имеет смысла пустой ROOT без строк)
|
||||||
|
SELECT
|
||||||
|
'ROOT'::VARCHAR AS row_type, 0 AS depth, NULL::INT AS line_id,
|
||||||
|
jsonb_build_object('section_code', sec_code, 'name', sec_name, 'expense_item_id', sec_id) AS header,
|
||||||
|
plan_data, seq_dfip_data, approved_data,
|
||||||
|
contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
|
||||||
|
contract_data, booking_data,
|
||||||
|
q1_data, q2_data, q3_data, q4_data, totals_data,
|
||||||
|
ARRAY[sec_id, 0, 0, 0]::INT[] AS sort_path
|
||||||
|
FROM root_agg
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- GROUP — из агрегатов (программы с бюджетными строками)
|
||||||
|
SELECT
|
||||||
|
'GROUP', 1, NULL,
|
||||||
|
jsonb_build_object('section_code', sec_code, 'name', prog_name, 'program_id', prog_id, 'expense_item_id', sec_id),
|
||||||
|
plan_data, seq_dfip_data, approved_data,
|
||||||
|
contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
|
||||||
|
contract_data, booking_data,
|
||||||
|
q1_data, q2_data, q3_data, q4_data, totals_data,
|
||||||
|
ARRAY[sec_id, prog_id, 0, 0]::INT[]
|
||||||
|
FROM prog_agg
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- GROUP — пустые программы (нет ни одной budget_line)
|
||||||
|
SELECT
|
||||||
|
'GROUP', 1, NULL,
|
||||||
|
jsonb_build_object('section_code', sec_code, 'name', prog_name, 'program_id', prog_id, 'expense_item_id', sec_id),
|
||||||
|
NULL::jsonb, NULL::jsonb, NULL::jsonb, -- plan, seq_dfip, approved
|
||||||
|
NULL::jsonb, NULL::jsonb, NULL::jsonb, -- contract_summary, allocation, reserve
|
||||||
|
NULL::jsonb, NULL::jsonb, -- collegial, ckk
|
||||||
|
NULL::jsonb, NULL::jsonb, -- contract, booking
|
||||||
|
NULL::jsonb, NULL::jsonb, NULL::jsonb, NULL::jsonb, NULL::jsonb, -- q1-q4, totals
|
||||||
|
ARRAY[sec_id, prog_id, 0, 0]::INT[]
|
||||||
|
FROM all_programs ap
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM v3.budget_line bl
|
||||||
|
WHERE bl.budget_form_id = p_form_id
|
||||||
|
AND (
|
||||||
|
bl.project_id = ap.prog_id -- прямая привязка к программе
|
||||||
|
OR bl.project_id IN (SELECT id FROM v3.form4_project pr -- через проект
|
||||||
|
WHERE pr.parent_id = ap.prog_id AND pr.level = 'project')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- ITEM — из агрегатов (проекты с бюджетными строками)
|
||||||
|
SELECT
|
||||||
|
'ITEM', 2, NULL,
|
||||||
|
jsonb_build_object('section_code', sec_code, 'name', prj_name,
|
||||||
|
'program_id', prog_id, 'project_id', prj_id, 'expense_item_id', sec_id),
|
||||||
|
plan_data, seq_dfip_data, approved_data,
|
||||||
|
contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
|
||||||
|
contract_data, booking_data,
|
||||||
|
q1_data, q2_data, q3_data, q4_data, totals_data,
|
||||||
|
ARRAY[sec_id, prog_id, prj_id, 0]::INT[]
|
||||||
|
FROM prj_agg
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- ITEM — пустые проекты (нет budget_line)
|
||||||
|
SELECT
|
||||||
|
'ITEM', 2, NULL,
|
||||||
|
jsonb_build_object('section_code', sec_code, 'name', prj_name,
|
||||||
|
'program_id', prog_id, 'project_id', prj_id, 'expense_item_id', sec_id),
|
||||||
|
NULL::jsonb, NULL::jsonb, NULL::jsonb,
|
||||||
|
NULL::jsonb, NULL::jsonb, NULL::jsonb,
|
||||||
|
NULL::jsonb, NULL::jsonb,
|
||||||
|
NULL::jsonb, NULL::jsonb,
|
||||||
|
NULL::jsonb, NULL::jsonb, NULL::jsonb, NULL::jsonb, NULL::jsonb,
|
||||||
|
ARRAY[sec_id, prog_id, prj_id, 0]::INT[]
|
||||||
|
FROM all_projects apr
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM v3.budget_line bl
|
||||||
|
WHERE bl.budget_form_id = p_form_id
|
||||||
|
AND bl.project_id = apr.prj_id
|
||||||
|
)
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- INPUT — все строки бюджета
|
||||||
|
SELECT
|
||||||
|
'INPUT', 3, line_id,
|
||||||
|
header || jsonb_build_object(
|
||||||
|
'project_id', prj_id, 'project_name', prj_name,
|
||||||
|
'program_id', prog_id, 'program_name', prog_name
|
||||||
|
),
|
||||||
|
plan_data, seq_dfip_data, approved_data,
|
||||||
|
contract_summary_data, allocation_data, reserve_data, collegial_data, ckk_data,
|
||||||
|
contract_data, booking_data,
|
||||||
|
q1_data, q2_data, q3_data, q4_data, totals_data,
|
||||||
|
ARRAY[sec_id, COALESCE(prog_id, 999999), COALESCE(prj_id, 999999), line_id]::INT[]
|
||||||
|
FROM inp
|
||||||
|
)
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
u.row_type, u.depth,
|
||||||
|
ROW_NUMBER() OVER (ORDER BY u.sort_path) AS sort_order,
|
||||||
|
jsonb_build_object(
|
||||||
|
'line_id', u.line_id,
|
||||||
|
'header', u.header,
|
||||||
|
'plan', u.plan_data,
|
||||||
|
'seq_dfip', u.seq_dfip_data,
|
||||||
|
'approved', u.approved_data,
|
||||||
|
'contract_summary', u.contract_summary_data,
|
||||||
|
'allocation', u.allocation_data,
|
||||||
|
'reserve', u.reserve_data,
|
||||||
|
'collegial', u.collegial_data,
|
||||||
|
'ckk', u.ckk_data,
|
||||||
|
'contract', u.contract_data,
|
||||||
|
'booking', u.booking_data,
|
||||||
|
'q1', u.q1_data,
|
||||||
|
'q2', u.q2_data,
|
||||||
|
'q3', u.q3_data,
|
||||||
|
'q4', u.q4_data,
|
||||||
|
'totals', u.totals_data
|
||||||
|
) AS data
|
||||||
|
FROM unioned u
|
||||||
|
ORDER BY u.sort_path;
|
||||||
|
$function$
|
||||||
|
LANGUAGE sql STABLE;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- v3.add_budget_line — создать новую INPUT-строку (budget_line) для FORM_1/2/4.
|
||||||
|
--
|
||||||
|
-- Способы передать expense_item:
|
||||||
|
-- путь A — численный id: p_expense_item_id (берётся из header.expense_item_id)
|
||||||
|
-- путь B — резолв по коду: p_sheet + p_item_id [+ p_section_code для коллизий]
|
||||||
|
-- [+ p_direction для FORM_1]
|
||||||
|
--
|
||||||
|
-- Авто-direction:
|
||||||
|
-- FORM_2 → 'Support'
|
||||||
|
-- FORM_4 → 'Development'
|
||||||
|
-- FORM_1/OPER → NULL
|
||||||
|
-- FORM_1/AHR|CAP → p_direction обязателен (Support|Development)
|
||||||
|
-- Если p_direction передан и противоречит авто-значению — ошибка.
|
||||||
|
--
|
||||||
|
-- Дубликаты на одном expense_item разрешены (UNIQUE не накладывается).
|
||||||
|
-- vsp_id опционален — для FORM_2 фронт может не знать ВСП на момент
|
||||||
|
-- создания, дозаполнит позже через upd_form_cell(.., 'header.vsp_id', ..).
|
||||||
|
--
|
||||||
|
-- FORM_3 не поддерживается — там строки живут в rf_project_report_line,
|
||||||
|
-- API будет в add_form3_line.sql (отдельная итерация).
|
||||||
|
--
|
||||||
|
-- Возврат: вся иерархия (ROOT/GROUP/ITEM/SUB_ITEM/SECTION/LEAF) + новая
|
||||||
|
-- INPUT (отфильтрована по новому line_id). Новый id извлекается из
|
||||||
|
-- (data->>'line_id')::INT первой INPUT-строки.
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS v3.add_budget_line(INT, INT, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, INT, VARCHAR);
|
||||||
|
DROP FUNCTION IF EXISTS v3.add_budget_line(INT, INT, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, VARCHAR, INT, INT, VARCHAR, VARCHAR, DATE);
|
||||||
|
|
||||||
|
CREATE FUNCTION v3.add_budget_line(
|
||||||
|
p_form_id INT,
|
||||||
|
p_expense_item_id INT DEFAULT NULL,
|
||||||
|
p_sheet VARCHAR DEFAULT NULL,
|
||||||
|
p_item_id VARCHAR DEFAULT NULL,
|
||||||
|
p_section_code VARCHAR DEFAULT NULL,
|
||||||
|
p_direction VARCHAR DEFAULT NULL,
|
||||||
|
p_name VARCHAR DEFAULT NULL,
|
||||||
|
p_internal_order VARCHAR DEFAULT NULL,
|
||||||
|
p_vsp_id INT DEFAULT NULL,
|
||||||
|
p_project_id INT DEFAULT NULL,
|
||||||
|
p_justification VARCHAR DEFAULT NULL,
|
||||||
|
-- сателлитные листы (AHR_RENT/UTILITY/SECURITY) используют дополнительно:
|
||||||
|
p_contract_number VARCHAR DEFAULT NULL,
|
||||||
|
p_contract_end_date DATE DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
row_type VARCHAR,
|
||||||
|
depth INT,
|
||||||
|
sort_order BIGINT,
|
||||||
|
data JSONB
|
||||||
|
)
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_form_type VARCHAR;
|
||||||
|
v_eid INT;
|
||||||
|
v_sheet VARCHAR;
|
||||||
|
v_auto_dir VARCHAR;
|
||||||
|
v_final_dir VARCHAR;
|
||||||
|
v_new_id INT;
|
||||||
|
v_cnt INT;
|
||||||
|
v_sat_table TEXT;
|
||||||
|
v_sat_eid INT;
|
||||||
|
v_parent_line INT;
|
||||||
|
BEGIN
|
||||||
|
-- 1. form_id → form_type
|
||||||
|
SELECT form_type_code INTO v_form_type FROM v3.budget_form WHERE id = p_form_id;
|
||||||
|
IF v_form_type IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'budget_form id=% не существует', p_form_id;
|
||||||
|
END IF;
|
||||||
|
IF v_form_type = 'FORM_3' THEN
|
||||||
|
RAISE EXCEPTION 'FORM_3 не использует budget_line — см. add_form3_line (TBD)';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ═══ Сателлитные листы (AHR_RENT/UTILITY/SECURITY) ═══════════════════
|
||||||
|
-- Фиксированный expense_item_id на лист — UI не передаёт R-код.
|
||||||
|
-- p_vsp_id обязателен. Опц.: p_contract_number, p_contract_end_date.
|
||||||
|
-- Внутри: находим/создаём родительскую budget_line с фиксированным ei,
|
||||||
|
-- затем INSERT в *_detail. Возврат — одна новая LEAF.
|
||||||
|
IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN
|
||||||
|
v_sat_table := CASE p_sheet
|
||||||
|
WHEN 'AHR_RENT' THEN 'rent_detail'
|
||||||
|
WHEN 'AHR_UTILITY' THEN 'utility_detail'
|
||||||
|
WHEN 'AHR_SECURITY' THEN 'security_detail'
|
||||||
|
END;
|
||||||
|
v_sat_eid := CASE p_sheet
|
||||||
|
WHEN 'AHR_RENT' THEN 293 -- R061031001 "Аренда_ОН"
|
||||||
|
WHEN 'AHR_UTILITY' THEN 24 -- 1.05.1. "Коммунальные услуги"
|
||||||
|
WHEN 'AHR_SECURITY' THEN 779 -- R061062001 "Пультовая охр.ОН"
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF p_vsp_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_vsp_id обязателен для сателлитного листа %', p_sheet;
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN
|
||||||
|
RAISE EXCEPTION 'vsp id=% не существует', p_vsp_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Находим/создаём родительскую budget_line под фиксированным ei
|
||||||
|
SELECT id INTO v_parent_line
|
||||||
|
FROM v3.budget_line
|
||||||
|
WHERE budget_form_id = p_form_id AND expense_item_id = v_sat_eid
|
||||||
|
LIMIT 1;
|
||||||
|
IF v_parent_line IS NULL THEN
|
||||||
|
INSERT INTO v3.budget_line (budget_form_id, expense_item_id, direction)
|
||||||
|
VALUES (
|
||||||
|
p_form_id, v_sat_eid,
|
||||||
|
CASE v_form_type WHEN 'FORM_2' THEN 'Support' WHEN 'FORM_4' THEN 'Development' ELSE NULL END
|
||||||
|
) RETURNING id INTO v_parent_line;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- INSERT в сателлит
|
||||||
|
EXECUTE format(
|
||||||
|
'INSERT INTO v3.%I (line_id, vsp_id, contract_number, contract_end_date) '
|
||||||
|
|| 'VALUES ($1, $2, $3, $4) RETURNING id',
|
||||||
|
v_sat_table
|
||||||
|
) USING v_parent_line, p_vsp_id, p_contract_number, p_contract_end_date
|
||||||
|
INTO v_new_id;
|
||||||
|
|
||||||
|
-- Возврат — одна новая INPUT-строка (отфильтрована по новому id сателлита)
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT v.row_type, v.depth, v.sort_order, v.data
|
||||||
|
FROM v3.v_form_view(p_form_id, p_sheet, NULL, NULL) v
|
||||||
|
WHERE v.row_type = 'INPUT' AND (v.data->>'id')::INT = v_new_id;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 2. Резолв expense_item_id
|
||||||
|
IF p_expense_item_id IS NOT NULL THEN
|
||||||
|
SELECT ei.id, ei.sheet INTO v_eid, v_sheet
|
||||||
|
FROM v3.expense_item ei WHERE ei.id = p_expense_item_id;
|
||||||
|
IF v_eid IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'expense_item id=% не существует', p_expense_item_id;
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM v3.expense_item_form_type
|
||||||
|
WHERE expense_item_id = v_eid AND form_type_code = v_form_type
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION 'expense_item id=% не привязан к form_type %', v_eid, v_form_type;
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
IF p_sheet IS NULL OR p_item_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'нужен либо p_expense_item_id, либо (p_sheet + p_item_id)';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT count(*) INTO v_cnt
|
||||||
|
FROM v3.expense_item ei
|
||||||
|
JOIN v3.expense_item_form_type eift ON eift.expense_item_id = ei.id
|
||||||
|
WHERE ei.item_id = p_item_id
|
||||||
|
AND ei.sheet = p_sheet
|
||||||
|
AND eift.form_type_code = v_form_type
|
||||||
|
AND (p_section_code IS NULL OR ei.section_code = p_section_code)
|
||||||
|
AND (p_direction IS NULL OR ei.direction IS NULL OR ei.direction = p_direction);
|
||||||
|
|
||||||
|
IF v_cnt = 0 THEN
|
||||||
|
RAISE EXCEPTION 'expense_item не найден: sheet=%, item_id=%, section=%, direction=%, form=%',
|
||||||
|
p_sheet, p_item_id, p_section_code, p_direction, v_form_type;
|
||||||
|
END IF;
|
||||||
|
IF v_cnt > 1 THEN
|
||||||
|
RAISE EXCEPTION 'expense_item не уникален (% совпадений): sheet=%, item_id=%, section=%, direction=%. Уточните p_section_code или передайте p_expense_item_id напрямую',
|
||||||
|
v_cnt, p_sheet, p_item_id, p_section_code, p_direction;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT ei.id, ei.sheet INTO v_eid, v_sheet
|
||||||
|
FROM v3.expense_item ei
|
||||||
|
JOIN v3.expense_item_form_type eift ON eift.expense_item_id = ei.id
|
||||||
|
WHERE ei.item_id = p_item_id
|
||||||
|
AND ei.sheet = p_sheet
|
||||||
|
AND eift.form_type_code = v_form_type
|
||||||
|
AND (p_section_code IS NULL OR ei.section_code = p_section_code)
|
||||||
|
AND (p_direction IS NULL OR ei.direction IS NULL OR ei.direction = p_direction);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 3. Авто-direction (для FORM_1/AHR|CAP — обязателен у юзера)
|
||||||
|
v_auto_dir := CASE
|
||||||
|
WHEN v_form_type = 'FORM_2' THEN 'Support'
|
||||||
|
WHEN v_form_type = 'FORM_4' THEN 'Development'
|
||||||
|
WHEN v_form_type = 'FORM_1' AND v_sheet = 'OPER' THEN NULL
|
||||||
|
ELSE NULL
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF v_form_type = 'FORM_1' AND v_sheet IN ('AHR','CAP') AND p_direction IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_direction обязателен для FORM_1 / sheet=% (Support|Development)', v_sheet;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_form_type IN ('FORM_2','FORM_4') AND p_direction IS NOT NULL AND p_direction <> v_auto_dir THEN
|
||||||
|
RAISE EXCEPTION 'p_direction=% несовместим с form_type=% (фиксирован=%)',
|
||||||
|
p_direction, v_form_type, v_auto_dir;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_form_type = 'FORM_1' AND v_sheet = 'OPER' AND p_direction IS NOT NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_direction должен быть NULL для FORM_1 / sheet=OPER (получено: %)', p_direction;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
v_final_dir := COALESCE(p_direction, v_auto_dir);
|
||||||
|
|
||||||
|
-- 4. Sanity на vsp/project
|
||||||
|
IF p_vsp_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN
|
||||||
|
RAISE EXCEPTION 'vsp id=% не существует', p_vsp_id;
|
||||||
|
END IF;
|
||||||
|
IF p_project_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.form4_project WHERE id = p_project_id) THEN
|
||||||
|
RAISE EXCEPTION 'form4_project id=% не существует', p_project_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 5. INSERT
|
||||||
|
INSERT INTO v3.budget_line (
|
||||||
|
budget_form_id, expense_item_id, name, internal_order,
|
||||||
|
vsp_id, project_id, direction, justification
|
||||||
|
) VALUES (
|
||||||
|
p_form_id, v_eid, p_name, p_internal_order,
|
||||||
|
p_vsp_id, p_project_id, v_final_dir, p_justification
|
||||||
|
) RETURNING id INTO v_new_id;
|
||||||
|
|
||||||
|
-- 5а. Аудит: ROW_CREATE
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'ROW_CREATE', 'ROW_CREATE',
|
||||||
|
jsonb_build_object(
|
||||||
|
'sheet', v_sheet,
|
||||||
|
'line_id', v_new_id,
|
||||||
|
'expense_item_id', v_eid,
|
||||||
|
'direction', v_final_dir,
|
||||||
|
'name', p_name,
|
||||||
|
'vsp_id', p_vsp_id,
|
||||||
|
'project_id', p_project_id
|
||||||
|
),
|
||||||
|
p_form_id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 6. Возврат: только иерархия по пути нового ei + новая INPUT
|
||||||
|
DECLARE v_ei_path INT[];
|
||||||
|
BEGIN
|
||||||
|
v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]);
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT v.row_type, v.depth, v.sort_order, v.data
|
||||||
|
FROM v3.v_form_view(p_form_id, v_sheet, NULL, v_final_dir) v
|
||||||
|
WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
|
||||||
|
AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
|
||||||
|
OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = v_new_id)
|
||||||
|
ORDER BY v.sort_order;
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql VOLATILE;
|
||||||
@ -14,3 +14,4 @@ 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:
|
if user is None or not user.is_active:
|
||||||
user = await user_repo.get_by_email(username)
|
user = await user_repo.get_by_email(username)
|
||||||
if user is None:
|
if user is None or not user.is_active:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Пользователь не найден",
|
detail="Пользователь не найден",
|
||||||
|
|||||||
@ -2,11 +2,13 @@
|
|||||||
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
|
from src.api.v1.deps import require_admin, require_executor
|
||||||
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,
|
||||||
)
|
)
|
||||||
@ -33,3 +35,28 @@ 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,10 +1,19 @@
|
|||||||
|
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,
|
||||||
@ -20,6 +29,18 @@ 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,
|
||||||
@ -29,14 +50,30 @@ 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=[FormPhaseResponse.model_validate(p) for p in phases],
|
result=result,
|
||||||
count=len(phases),
|
count=len(phases),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -53,13 +90,25 @@ 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}")
|
||||||
@ -84,9 +133,21 @@ 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}")
|
||||||
@ -111,3 +172,173 @@ async def delete_form_phase(
|
|||||||
)
|
)
|
||||||
if not deleted:
|
if not deleted:
|
||||||
raise HTTPException(404, "Этап не найден")
|
raise HTTPException(404, "Этап не найден")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/project/{project_id}")
|
||||||
|
async def get_form3_phases(
|
||||||
|
project_id: int,
|
||||||
|
year: int | None = None,
|
||||||
|
report_type: str | None = None,
|
||||||
|
phase_code: str | None = None,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||||
|
) -> BaseListResponse[Form3PhaseResponse]:
|
||||||
|
f3p_service = Form3PhaseService(db)
|
||||||
|
report_service = RfProjectReportService(db)
|
||||||
|
project_service = ProjectService(db)
|
||||||
|
org_unit_service = OrgUnitService(db)
|
||||||
|
|
||||||
|
reports = await report_service.get_list(
|
||||||
|
project_id=project_id,
|
||||||
|
year=year,
|
||||||
|
report_type=report_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
phases = await f3p_service.get_list(
|
||||||
|
report_id=[el.id for el in reports],
|
||||||
|
phase_code=phase_code,
|
||||||
|
)
|
||||||
|
if phases:
|
||||||
|
project = await project_service.get_instance(user=current_user, project_id=project_id)
|
||||||
|
org_unit = await org_unit_service.get(user=current_user, org_unit_id=project.org_unit_id)
|
||||||
|
result = [Form3PhaseResponse.model_validate(p) for p in phases]
|
||||||
|
if org_unit.utc_offset:
|
||||||
|
tz = timezone_from_offset(org_unit.utc_offset)
|
||||||
|
for element in result:
|
||||||
|
if element.opens_at:
|
||||||
|
element.opens_at = element.opens_at.astimezone(tz)
|
||||||
|
if element.closes_at:
|
||||||
|
element.closes_at = element.closes_at.astimezone(tz)
|
||||||
|
else:
|
||||||
|
result = []
|
||||||
|
if result:
|
||||||
|
reports = {report.id: report for report in reports}
|
||||||
|
for i, element in enumerate(result):
|
||||||
|
element.year = reports[phases[i].rf_project_report_id].year
|
||||||
|
element.report_type = reports[phases[i].rf_project_report_id].report_type
|
||||||
|
return BaseListResponse(
|
||||||
|
result=result,
|
||||||
|
count=len(phases),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/project/{project_id}")
|
||||||
|
async def create_form3_phase(
|
||||||
|
project_id: int,
|
||||||
|
body: Form3PhaseCreate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(require_admin),
|
||||||
|
) -> BaseSingleResponse[Form3PhaseResponse]:
|
||||||
|
report_service = RfProjectReportService(db)
|
||||||
|
report = await report_service.get(
|
||||||
|
project_id=project_id,
|
||||||
|
report_type=body.report_type,
|
||||||
|
year=body.year,
|
||||||
|
user=current_user,
|
||||||
|
load_project=True,
|
||||||
|
)
|
||||||
|
if not report:
|
||||||
|
raise HTTPException(404, "Форма не найдена")
|
||||||
|
|
||||||
|
org_unit_service = OrgUnitService(db)
|
||||||
|
org_unit = await org_unit_service.get(user=current_user, org_unit_id=report.project.org_unit_id)
|
||||||
|
|
||||||
|
f3p_service = Form3PhaseService(db)
|
||||||
|
phase = await f3p_service.create(
|
||||||
|
report_id=report.id,
|
||||||
|
body=body,
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = Form3PhaseResponse.model_validate(phase)
|
||||||
|
|
||||||
|
if org_unit.utc_offset:
|
||||||
|
tz = timezone_from_offset(org_unit.utc_offset)
|
||||||
|
if result.opens_at:
|
||||||
|
result.opens_at = result.opens_at.astimezone(tz)
|
||||||
|
if result.closes_at:
|
||||||
|
result.closes_at = result.closes_at.astimezone(tz)
|
||||||
|
|
||||||
|
result.year = body.year
|
||||||
|
result.report_type = body.report_type
|
||||||
|
|
||||||
|
return BaseSingleResponse(result=result)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/project/{project_id}/{year}/{report_type}/{phase_code}")
|
||||||
|
async def update_form3_phase(
|
||||||
|
project_id: int,
|
||||||
|
year: int,
|
||||||
|
report_type: str,
|
||||||
|
phase_code: str,
|
||||||
|
body: Form3PhaseUpdate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(require_admin),
|
||||||
|
) -> BaseSingleResponse[Form3PhaseResponse]:
|
||||||
|
report_service = RfProjectReportService(db)
|
||||||
|
report = await report_service.get(
|
||||||
|
project_id=project_id,
|
||||||
|
report_type=report_type,
|
||||||
|
year=year,
|
||||||
|
user=current_user,
|
||||||
|
load_project=True,
|
||||||
|
)
|
||||||
|
if not report:
|
||||||
|
raise HTTPException(404, "Форма не найдена")
|
||||||
|
|
||||||
|
f3p_service = Form3PhaseService(db)
|
||||||
|
phase = await f3p_service.update(
|
||||||
|
report_id=report.id,
|
||||||
|
phase_code=phase_code,
|
||||||
|
body=body,
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not phase:
|
||||||
|
raise HTTPException(404, "Этап не найден")
|
||||||
|
|
||||||
|
org_unit_service = OrgUnitService(db)
|
||||||
|
org_unit = await org_unit_service.get(user=current_user, org_unit_id=report.project.org_unit_id)
|
||||||
|
result = Form3PhaseResponse.model_validate(phase)
|
||||||
|
|
||||||
|
if org_unit.utc_offset:
|
||||||
|
tz = timezone_from_offset(org_unit.utc_offset)
|
||||||
|
if result.opens_at:
|
||||||
|
result.opens_at = result.opens_at.astimezone(tz)
|
||||||
|
if result.closes_at:
|
||||||
|
result.closes_at = result.closes_at.astimezone(tz)
|
||||||
|
|
||||||
|
result.year = year
|
||||||
|
result.report_type = report_type
|
||||||
|
|
||||||
|
return BaseSingleResponse(result=result)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/project/{project_id}/{year}/{report_type}/{phase_code}")
|
||||||
|
async def delete_form3_phase(
|
||||||
|
project_id: int,
|
||||||
|
year: int,
|
||||||
|
report_type: str,
|
||||||
|
phase_code: str,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(require_admin),
|
||||||
|
):
|
||||||
|
report_service = RfProjectReportService(db)
|
||||||
|
report = await report_service.get(
|
||||||
|
project_id=project_id,
|
||||||
|
report_type=report_type,
|
||||||
|
year=year,
|
||||||
|
user=current_user,
|
||||||
|
load_project=True,
|
||||||
|
)
|
||||||
|
if not report:
|
||||||
|
raise HTTPException(404, "Форма не найдена")
|
||||||
|
|
||||||
|
f3p_service = Form3PhaseService(db)
|
||||||
|
deleted = await f3p_service.delete(
|
||||||
|
report_id=report.id,
|
||||||
|
phase_code=phase_code,
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
if not deleted:
|
||||||
|
raise HTTPException(404, "Этап не найден")
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@ -20,6 +21,9 @@ 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(
|
||||||
@ -153,6 +157,20 @@ 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),
|
||||||
@ -188,12 +206,6 @@ 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,
|
||||||
@ -206,6 +218,22 @@ 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),
|
||||||
@ -243,14 +271,6 @@ 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,
|
||||||
@ -261,6 +281,21 @@ 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),
|
||||||
@ -310,6 +345,21 @@ 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),
|
||||||
@ -351,6 +401,21 @@ 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),
|
||||||
@ -376,7 +441,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(
|
new_forms = await bf_service.bulk_create_if_not_exists(
|
||||||
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,
|
||||||
@ -384,6 +449,18 @@ 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, OrgUnitBaseSchema, OrgUnitListSchema, OrgUnitSchema, OrgUnitUpdateSchema, ResponseBase
|
from src.domain.schemas import BaseListResponse, BaseSingleResponse, OrgUnitCreateSchema, 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: OrgUnitBaseSchema,
|
org_data: OrgUnitCreateSchema,
|
||||||
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,6 +75,7 @@ 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,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@ -20,6 +21,9 @@ 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"}
|
||||||
|
|
||||||
@ -96,6 +100,20 @@ 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))
|
||||||
|
|
||||||
|
|
||||||
@ -118,6 +136,19 @@ 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,
|
||||||
location_form: str | None = None,
|
placement_type: str | None = None,
|
||||||
numbers_min: int | None = None,
|
staff_count_min: int | None = None,
|
||||||
numbers_max: int | None = None,
|
staff_count_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,
|
||||||
location_form=location_form,
|
placement_type=placement_type,
|
||||||
numbers_min=numbers_min,
|
staff_count_min=staff_count_min,
|
||||||
numbers_max=numbers_max,
|
staff_count_max=staff_count_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,
|
||||||
location_form=body.location_form,
|
placement_type=body.placement_type,
|
||||||
numbers_min=body.numbers_min,
|
staff_count_min=body.staff_count_min,
|
||||||
numbers_max=body.numbers_max,
|
staff_count_max=body.staff_count_max,
|
||||||
)
|
)
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
stream,
|
stream,
|
||||||
|
|||||||
@ -4,6 +4,8 @@ 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
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -12,6 +14,9 @@ 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
|
||||||
@ -26,6 +31,9 @@ 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):
|
||||||
"""Контекстный менеджер для получения сессии базы данных."""
|
"""Контекстный менеджер для получения сессии базы данных."""
|
||||||
@ -265,7 +273,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_locked_by_other(
|
def is_cell_available_for_edit(
|
||||||
self,
|
self,
|
||||||
con_key: ConnectionKeyEnum,
|
con_key: ConnectionKeyEnum,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
@ -273,10 +281,10 @@ class ConnectionManager:
|
|||||||
form_key,
|
form_key,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not cell_key:
|
if not cell_key:
|
||||||
return False
|
return True
|
||||||
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,
|
||||||
@ -369,6 +377,18 @@ class FormEventProcess:
|
|||||||
sheet=sheet,
|
sheet=sheet,
|
||||||
direction=direction,
|
direction=direction,
|
||||||
)
|
)
|
||||||
|
case "program_added":
|
||||||
|
return await self.__add_program(
|
||||||
|
event_data=event_data["data"],
|
||||||
|
form_id=form_id,
|
||||||
|
user=curr_user,
|
||||||
|
sheet=sheet,
|
||||||
|
)
|
||||||
|
case "project_added":
|
||||||
|
return await self.__add_project(
|
||||||
|
event_data=event_data["data"],
|
||||||
|
user=curr_user,
|
||||||
|
)
|
||||||
case _:
|
case _:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -455,6 +475,41 @@ 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,
|
||||||
@ -485,7 +540,8 @@ class FormEventProcess:
|
|||||||
class ProjectEventProcess:
|
class ProjectEventProcess:
|
||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
self.project_service: ProjectService = ProjectService(db)
|
self.project_service: ProjectService = ProjectService(db)
|
||||||
self.bf_service: BudgetFormService = BudgetFormService(db)
|
self.prl_service: RfProjectReportLineService = RfProjectReportLineService(db)
|
||||||
|
self.pr_service: RfProjectReportService = RfProjectReportService(db)
|
||||||
self.user_service: UserService = UserService(db)
|
self.user_service: UserService = UserService(db)
|
||||||
|
|
||||||
async def process(
|
async def process(
|
||||||
@ -568,11 +624,16 @@ class ProjectEventProcess:
|
|||||||
expense_item_id: Optional[int] = None
|
expense_item_id: Optional[int] = None
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
project = await self.project_service.get(project_id=project_id, user=user)
|
project_report = await self.pr_service.get(
|
||||||
if not project:
|
year=year,
|
||||||
|
report_type=report_type,
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
if not project_report:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await self.project_service.add_form3_line(
|
ids = await self.prl_service.get_ids(project_report_id=project_report.id)
|
||||||
|
result = await self.project_service.add_form3_line(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
year=year,
|
year=year,
|
||||||
report_type=report_type,
|
report_type=report_type,
|
||||||
@ -580,6 +641,16 @@ class ProjectEventProcess:
|
|||||||
user=user,
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
final_result = {
|
||||||
|
"data": result,
|
||||||
|
"new_line_id": None,
|
||||||
|
}
|
||||||
|
for el in result:
|
||||||
|
if el[3]["line_id"] and el[3]["line_id"] not in ids:
|
||||||
|
final_result["new_line_id"] = el[3]["line_id"]
|
||||||
|
return final_result
|
||||||
|
return final_result
|
||||||
|
|
||||||
async def __del_row(
|
async def __del_row(
|
||||||
self,
|
self,
|
||||||
event_data: dict,
|
event_data: dict,
|
||||||
@ -590,7 +661,7 @@ class ProjectEventProcess:
|
|||||||
) -> list[tuple]:
|
) -> list[tuple]:
|
||||||
"""
|
"""
|
||||||
data = {
|
data = {
|
||||||
"line_id": int,
|
"row_id": int,
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
project = await self.project_service.get(project_id=project_id, user=user)
|
project = await self.project_service.get(project_id=project_id, user=user)
|
||||||
@ -601,7 +672,7 @@ class ProjectEventProcess:
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
year=year,
|
year=year,
|
||||||
report_type=report_type,
|
report_type=report_type,
|
||||||
line_id=event_data["line_id"],
|
line_id=event_data["row_id"],
|
||||||
user=user,
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -639,7 +710,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["line_id"],
|
"line_id": data.get("line_id_code", data["line_id"]),
|
||||||
"column": data["column"],
|
"column": data["column"],
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -727,19 +798,7 @@ async def process_websocket(
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
case "cell_updated":
|
case "cell_updated":
|
||||||
if manager.is_locked_by_other(
|
if not manager.is_cell_available_for_edit(
|
||||||
con_key=con_key,
|
|
||||||
user_id=user_id,
|
|
||||||
cell_key=cell_key,
|
|
||||||
form_key=kwargs_process,
|
|
||||||
):
|
|
||||||
data["error"] = "Ячейка уже редактируется другим пользователем"
|
|
||||||
await manager.send_back(
|
|
||||||
data,
|
|
||||||
websocket,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
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,
|
||||||
@ -751,6 +810,18 @@ async def process_websocket(
|
|||||||
websocket,
|
websocket,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
# if cell_key and not manager.acquire_cell_lock(
|
||||||
|
# con_key=con_key,
|
||||||
|
# user_id=user_id,
|
||||||
|
# cell_key=cell_key,
|
||||||
|
# form_key=kwargs_process,
|
||||||
|
# ):
|
||||||
|
# data["error"] = "Ячейка уже редактируется другим пользователем"
|
||||||
|
# await manager.send_back(
|
||||||
|
# data,
|
||||||
|
# websocket,
|
||||||
|
# )
|
||||||
|
# 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,
|
||||||
@ -770,12 +841,26 @@ 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(
|
||||||
@ -787,6 +872,8 @@ async def process_websocket(
|
|||||||
if data.get("event") in [
|
if data.get("event") in [
|
||||||
"cell_updated",
|
"cell_updated",
|
||||||
"row_added",
|
"row_added",
|
||||||
|
"program_added",
|
||||||
|
"project_added",
|
||||||
"row_deleted",
|
"row_deleted",
|
||||||
]:
|
]:
|
||||||
await manager.broadcast_to_all(
|
await manager.broadcast_to_all(
|
||||||
@ -815,6 +902,12 @@ async def process_websocket(
|
|||||||
data=data,
|
data=data,
|
||||||
websocket=websocket,
|
websocket=websocket,
|
||||||
)
|
)
|
||||||
|
except SheetValidationError as e:
|
||||||
|
data["error"] = e.message
|
||||||
|
await manager.send_back(
|
||||||
|
data=data,
|
||||||
|
websocket=websocket,
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
tp = type(e)
|
tp = type(e)
|
||||||
|
|||||||
@ -9,7 +9,9 @@ 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
|
from src.db.models.expense_item import ExpenseItem, ExpenseItemFormType
|
||||||
|
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,3 +25,12 @@ 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)
|
||||||
|
|||||||
27
api/src/db/models/form3_phase.py
Normal file
27
api/src/db/models/form3_phase.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import ARRAY, CheckConstraint, DateTime, ForeignKey, Index, Integer, String, Text, text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from src.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Form3Phase(Base):
|
||||||
|
__tablename__ = "form3_phase"
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint(text("cardinality(column_keys) > 0"), name="chk_v3_form3_phase_columns"),
|
||||||
|
CheckConstraint(text("role != 'ADMIN'"), name="chk_v3_form3_phase_no_admin"),
|
||||||
|
CheckConstraint(text("closes_at > opens_at"), name="chk_v3_form3_phase_window"),
|
||||||
|
Index("ix_v3_form3_phase_active", "rf_project_report_id", "role", "opens_at", "closes_at"),
|
||||||
|
Index("ix_v3_form3_phase_columns_gin", "column_keys", postgresql_using="gin"),
|
||||||
|
{"schema": "v3"},
|
||||||
|
)
|
||||||
|
|
||||||
|
rf_project_report_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("v3.rf_project_report.id", ondelete="CASCADE"), primary_key=True
|
||||||
|
)
|
||||||
|
phase_code: Mapped[str] = mapped_column(String, primary_key=True)
|
||||||
|
role: Mapped[str] = mapped_column(String, ForeignKey("v3.role.code"))
|
||||||
|
column_keys: Mapped[list[str]] = mapped_column(ARRAY(Text))
|
||||||
|
opens_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||||
|
closes_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||||
54
api/src/db/models/form4_project.py
Normal file
54
api/src/db/models/form4_project.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import enum
|
||||||
|
|
||||||
|
from sqlalchemy import CheckConstraint, ForeignKey, Index, Integer, String, text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from src.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Form4ProjectLevelEnum(str, enum.Enum):
|
||||||
|
PROGRAM = "program"
|
||||||
|
PROJECT = "project"
|
||||||
|
|
||||||
|
|
||||||
|
class Form4Project(Base):
|
||||||
|
__tablename__ = "form4_project"
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint(
|
||||||
|
"level IN ('program', 'project')", name="chk_form4_project_level"
|
||||||
|
),
|
||||||
|
CheckConstraint(
|
||||||
|
"(level = 'program' AND parent_id IS NULL) OR (level = 'project')",
|
||||||
|
name="chk_form4_project_parent",
|
||||||
|
),
|
||||||
|
CheckConstraint(
|
||||||
|
"(level = 'program') = (section_code IS NOT NULL)",
|
||||||
|
name="chk_form4_project_section_code",
|
||||||
|
),
|
||||||
|
Index(
|
||||||
|
"ix_form4_project_section_code",
|
||||||
|
"section_code",
|
||||||
|
postgresql_where=text("level = 'program'"),
|
||||||
|
),
|
||||||
|
Index("ix_v3_form4_project_parent", "parent_id"),
|
||||||
|
Index("ix_v3_form4_project_form", "form_id"),
|
||||||
|
{"schema": "v3"},
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String)
|
||||||
|
level: Mapped[Form4ProjectLevelEnum] = mapped_column(String) # type: ignore
|
||||||
|
section_code: Mapped[str | None] = mapped_column(String)
|
||||||
|
parent_id: Mapped[int | None] = mapped_column(
|
||||||
|
Integer, ForeignKey("v3.form4_project.id")
|
||||||
|
)
|
||||||
|
form_id: Mapped[int] = mapped_column(
|
||||||
|
Integer, ForeignKey("v3.budget_form.id")
|
||||||
|
)
|
||||||
|
|
||||||
|
# parent = relationship("Form4Project", remote_side="Form4Project.id", back_populates="children")
|
||||||
|
# children: Mapped[list["Form4Project"]] = relationship("Form4Project", back_populates="parent") # type: ignore
|
||||||
|
# budget_form = relationship("BudgetForm")
|
||||||
|
# budget_lines: Mapped[list["BudgetLine"]] = relationship("BudgetLine", back_populates="form4_project") # type: ignore
|
||||||
@ -21,6 +21,7 @@ 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=True))
|
opens_at: Mapped[datetime] = mapped_column(DateTime(timezone=False))
|
||||||
closes_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
closes_at: Mapped[datetime] = mapped_column(DateTime(timezone=False))
|
||||||
|
|
||||||
# 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")
|
project: Mapped["Project"] = relationship("Project", back_populates="rf_project_reports") # type: ignore
|
||||||
# creator: Mapped[AppUser | None] = relationship("AppUser", foreign_keys=[created_by])
|
# creator: Mapped[AppUser | None] = relationship("AppUser", foreign_keys=[created_by])
|
||||||
# updater: Mapped[AppUser | None] = relationship("AppUser", foreign_keys=[updated_by])
|
# updater: Mapped[AppUser | None] = relationship("AppUser", foreign_keys=[updated_by])
|
||||||
# lines: Mapped[list[RfProjectReportLine]] = relationship("RfProjectReportLine", back_populates="report")
|
# lines: Mapped[list[RfProjectReportLine]] = relationship("RfProjectReportLine", back_populates="report")
|
||||||
|
|||||||
@ -45,8 +45,6 @@ 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,10 +233,16 @@ 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)
|
||||||
|
|
||||||
@ -244,6 +250,7 @@ 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)
|
||||||
@ -261,6 +268,7 @@ 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
|
||||||
@ -358,7 +366,7 @@ class AddProjectBody(BaseModel):
|
|||||||
max_length=30,
|
max_length=30,
|
||||||
pattern=r"^[^+\-\/\\=&*\s]{1,30}$",
|
pattern=r"^[^+\-\/\\=&*\s]{1,30}$",
|
||||||
)
|
)
|
||||||
year: int
|
year: int = Field(default_factory=lambda: datetime.now().year)
|
||||||
branch_id: int
|
branch_id: int
|
||||||
level: Literal["project", "program"] = "project"
|
level: Literal["project", "program"] = "project"
|
||||||
parent_id: Optional[int] = None
|
parent_id: Optional[int] = None
|
||||||
@ -370,6 +378,10 @@ 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
|
||||||
@ -413,6 +425,35 @@ class FormPhaseUpdate(BaseModel):
|
|||||||
closes_at: datetime | None = None
|
closes_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Form3PhaseResponse(BaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
phase_code: str
|
||||||
|
role: str
|
||||||
|
column_keys: list[str]
|
||||||
|
opens_at: datetime
|
||||||
|
closes_at: datetime
|
||||||
|
year: int | None = None
|
||||||
|
report_type: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Form3PhaseCreate(BaseModel):
|
||||||
|
year: int
|
||||||
|
report_type: str
|
||||||
|
phase_code: str
|
||||||
|
role: FormPhaseRole
|
||||||
|
column_keys: list[str]
|
||||||
|
opens_at: datetime
|
||||||
|
closes_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class Form3PhaseUpdate(BaseModel):
|
||||||
|
role: FormPhaseRole | None = None
|
||||||
|
column_keys: list[str] | None = None
|
||||||
|
opens_at: datetime | None = None
|
||||||
|
closes_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
class AuditLogBase(BaseModel):
|
class AuditLogBase(BaseModel):
|
||||||
"""Базовая схема записи аудита (единый формат вывода как у auditlog)."""
|
"""Базовая схема записи аудита (единый формат вывода как у auditlog)."""
|
||||||
|
|
||||||
@ -490,8 +531,8 @@ class VSPBase(BaseModel):
|
|||||||
serialization_alias='approved_format',
|
serialization_alias='approved_format',
|
||||||
)
|
)
|
||||||
notes: Optional[str] = Field(None, description="Примечания")
|
notes: Optional[str] = Field(None, description="Примечания")
|
||||||
location_form: Optional[str] = Field(None, description="Форма расположения")
|
placement_type: Optional[str] = Field(None, description="Тип размещения")
|
||||||
numbers: Optional[int] = Field(None, description="Штатная численность")
|
staff_count: Optional[int] = Field(None, description="Штатная численность")
|
||||||
area: Optional[float] = Field(
|
area: Optional[float] = Field(
|
||||||
None,
|
None,
|
||||||
description="Арендная площадь",
|
description="Арендная площадь",
|
||||||
@ -539,8 +580,8 @@ class VSPEditBase(BaseModel):
|
|||||||
serialization_alias='format',
|
serialization_alias='format',
|
||||||
)
|
)
|
||||||
notes: Optional[str] = Field(None, description="Примечания")
|
notes: Optional[str] = Field(None, description="Примечания")
|
||||||
location_form: Optional[str] = Field(None, description="Форма расположения")
|
placement_type: Optional[str] = Field(None, description="Тип размещения")
|
||||||
numbers: Optional[int] = Field(None, description="Штатная численность")
|
staff_count: Optional[int] = Field(None, description="Штатная численность")
|
||||||
total_area: Optional[float] = Field(
|
total_area: Optional[float] = Field(
|
||||||
None,
|
None,
|
||||||
description="Арендная площадь",
|
description="Арендная площадь",
|
||||||
@ -611,9 +652,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
|
||||||
location_form: Optional[str] = None
|
placement_type: Optional[str] = None
|
||||||
numbers_min: Optional[int] = None
|
staff_count_min: Optional[int] = None
|
||||||
numbers_max: Optional[int] = None
|
staff_count_max: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class ExpenseItemBaseSchema(BaseModel):
|
class ExpenseItemBaseSchema(BaseModel):
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
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
|
||||||
@ -20,6 +21,10 @@ 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,6 +1,7 @@
|
|||||||
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
|
||||||
|
|
||||||
@ -58,20 +59,35 @@ class BudgetFormRepository:
|
|||||||
|
|
||||||
async def get(
|
async def get(
|
||||||
self,
|
self,
|
||||||
budget_form_id: int,
|
budget_form_id: int | None = None,
|
||||||
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 = [BudgetForm.id == budget_form_id]
|
where = []
|
||||||
|
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]) -> BudgetLine | None:
|
async def get_list(self, budget_line_ids: list[int]) -> list[BudgetLine]:
|
||||||
query = select(BudgetLine).where(BudgetLine.id.in_(budget_line_ids))
|
query = select(BudgetLine).where(BudgetLine.id.in_(budget_line_ids))
|
||||||
return (await self.db.execute(query)).scalars().all()
|
return (await self.db.execute(query)).scalars().all()
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.db.models.expense_item import ExpenseItem
|
from src.db.models.form_type import FormTypeEnum
|
||||||
|
from src.db.models.expense_item import ExpenseItem, ExpenseItemFormType
|
||||||
|
|
||||||
|
|
||||||
class ExpenseItemRepository:
|
class ExpenseItemRepository:
|
||||||
@ -11,3 +12,32 @@ 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()
|
||||||
|
|||||||
116
api/src/repository/form3_phase_repository.py
Normal file
116
api/src/repository/form3_phase_repository.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import select, text
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.form3_phase import Form3Phase
|
||||||
|
|
||||||
|
|
||||||
|
class Form3PhaseRepository:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get_list(
|
||||||
|
self,
|
||||||
|
report_id: int | list[int],
|
||||||
|
phase_code: str | None = None,
|
||||||
|
) -> list[Form3Phase]:
|
||||||
|
if isinstance(report_id, int):
|
||||||
|
query = select(Form3Phase).where(Form3Phase.rf_project_report_id == report_id)
|
||||||
|
else:
|
||||||
|
query = select(Form3Phase).where(Form3Phase.rf_project_report_id.in_(report_id))
|
||||||
|
if phase_code is not None:
|
||||||
|
query = query.where(Form3Phase.phase_code == phase_code)
|
||||||
|
return (await self.db.execute(query)).scalars().all()
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self, report_id: int, phase_code: str
|
||||||
|
) -> Form3Phase | None:
|
||||||
|
query = (
|
||||||
|
select(Form3Phase)
|
||||||
|
.where(Form3Phase.rf_project_report_id == report_id)
|
||||||
|
.where(Form3Phase.phase_code == phase_code)
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
return (await self.db.execute(query)).scalar_one_or_none()
|
||||||
|
|
||||||
|
async def create(
|
||||||
|
self,
|
||||||
|
report_id: int,
|
||||||
|
phase_code: str,
|
||||||
|
role: str,
|
||||||
|
column_keys: list[str],
|
||||||
|
opens_at: datetime,
|
||||||
|
closes_at: datetime,
|
||||||
|
) -> Form3Phase:
|
||||||
|
result = await self.db.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
(v3.add_form3_phase(
|
||||||
|
:report_id,
|
||||||
|
:phase_code,
|
||||||
|
:role,
|
||||||
|
:column_keys,
|
||||||
|
CAST(:opens_at AS TIMESTAMP WITHOUT TIME ZONE),
|
||||||
|
CAST(:closes_at AS TIMESTAMP WITHOUT TIME ZONE)
|
||||||
|
)).rf_project_report_id
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"report_id": report_id,
|
||||||
|
"phase_code": phase_code,
|
||||||
|
"role": role,
|
||||||
|
"column_keys": column_keys,
|
||||||
|
"opens_at": opens_at,
|
||||||
|
"closes_at": closes_at,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if result.scalar_one_or_none() is None:
|
||||||
|
return None
|
||||||
|
return await self.get(report_id, phase_code)
|
||||||
|
|
||||||
|
async def update(
|
||||||
|
self, report_id: int, phase_code: str, data: dict
|
||||||
|
) -> Form3Phase | None:
|
||||||
|
result = await self.db.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
(v3.upd_form3_phase(
|
||||||
|
:report_id,
|
||||||
|
:phase_code,
|
||||||
|
:role,
|
||||||
|
:column_keys,
|
||||||
|
CAST(:opens_at AS TIMESTAMP WITHOUT TIME ZONE),
|
||||||
|
CAST(:closes_at AS TIMESTAMP WITHOUT TIME ZONE)
|
||||||
|
)).rf_project_report_id
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"report_id": report_id,
|
||||||
|
"phase_code": phase_code,
|
||||||
|
"role": data.get("role"),
|
||||||
|
"column_keys": data.get("column_keys"),
|
||||||
|
"opens_at": data.get("opens_at"),
|
||||||
|
"closes_at": data.get("closes_at"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if result.scalar_one_or_none() is None:
|
||||||
|
return None
|
||||||
|
return await self.get(report_id, phase_code)
|
||||||
|
|
||||||
|
async def delete(
|
||||||
|
self, report_id: int, phase_code: str
|
||||||
|
) -> bool:
|
||||||
|
result = await self.db.execute(
|
||||||
|
text(
|
||||||
|
"SELECT v3.del_form3_phase(:report_id, :phase_code)"
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"report_id": report_id,
|
||||||
|
"phase_code": phase_code,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
deleted = result.scalar_one_or_none()
|
||||||
|
return bool(deleted)
|
||||||
65
api/src/repository/form4_project_repository.py
Normal file
65
api/src/repository/form4_project_repository.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
from sqlalchemy import func, select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.form4_project import Form4Project, Form4ProjectLevelEnum
|
||||||
|
|
||||||
|
|
||||||
|
class Form4ProjectRepository:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self,
|
||||||
|
project_id: int,
|
||||||
|
level: Form4ProjectLevelEnum | str | None = None,
|
||||||
|
) -> Form4Project | None:
|
||||||
|
where = [Form4Project.id == project_id]
|
||||||
|
if level is not None:
|
||||||
|
where.append(Form4Project.level == level)
|
||||||
|
|
||||||
|
return (
|
||||||
|
(await self.db.execute(select(Form4Project).where(*where).limit(1)))
|
||||||
|
.scalars()
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_list(
|
||||||
|
self,
|
||||||
|
level: Form4ProjectLevelEnum | None = None,
|
||||||
|
form_id: int | None = None,
|
||||||
|
parent_id: int | None = None,
|
||||||
|
offset: int | None = None,
|
||||||
|
limit: int | None = None,
|
||||||
|
with_count: bool = False,
|
||||||
|
) -> list[Form4Project] | tuple[int, list[Form4Project]]:
|
||||||
|
if with_count:
|
||||||
|
query = select(func.count().over().label("total_count"), Form4Project)
|
||||||
|
else:
|
||||||
|
query = select(Form4Project)
|
||||||
|
|
||||||
|
where = []
|
||||||
|
if level is not None:
|
||||||
|
where.append(Form4Project.level == level)
|
||||||
|
if form_id is not None:
|
||||||
|
where.append(Form4Project.form_id == form_id)
|
||||||
|
if parent_id is not None:
|
||||||
|
where.append(Form4Project.parent_id == parent_id)
|
||||||
|
|
||||||
|
query = query.where(*where).order_by(Form4Project.id)
|
||||||
|
|
||||||
|
if offset is not None:
|
||||||
|
query = query.offset(offset)
|
||||||
|
if limit is not None:
|
||||||
|
query = query.limit(limit)
|
||||||
|
|
||||||
|
if with_count:
|
||||||
|
result = (await self.db.execute(query)).all()
|
||||||
|
if result:
|
||||||
|
return result[0][0], [res[1] for res in result]
|
||||||
|
else:
|
||||||
|
count_query = select(func.count(Form4Project.id))
|
||||||
|
if query.whereclause is not None:
|
||||||
|
count_query = count_query.where(query.whereclause)
|
||||||
|
return (await self.db.execute(count_query)).scalar(), []
|
||||||
|
else:
|
||||||
|
return (await self.db.execute(query)).scalars().all()
|
||||||
@ -55,8 +55,8 @@ class FormPhaseRepository:
|
|||||||
:phase_code,
|
:phase_code,
|
||||||
:role,
|
:role,
|
||||||
:column_keys,
|
:column_keys,
|
||||||
:opens_at,
|
CAST(:opens_at AS TIMESTAMP WITHOUT TIME ZONE),
|
||||||
:closes_at
|
CAST(:closes_at AS TIMESTAMP WITHOUT TIME ZONE)
|
||||||
)).budget_form_id
|
)).budget_form_id
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
@ -87,8 +87,8 @@ class FormPhaseRepository:
|
|||||||
:phase_code,
|
:phase_code,
|
||||||
:role,
|
:role,
|
||||||
:column_keys,
|
:column_keys,
|
||||||
:opens_at,
|
CAST(:opens_at AS TIMESTAMP WITHOUT TIME ZONE),
|
||||||
:closes_at
|
CAST(:closes_at AS TIMESTAMP WITHOUT TIME ZONE)
|
||||||
)).budget_form_id
|
)).budget_form_id
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
from sqlalchemy import func, select, update
|
from sqlalchemy import func, select, text, 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,9 +84,10 @@ 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)
|
ssp = OrgUnit(title=title, is_ssp=is_ssp, utc_offset=utc_offset)
|
||||||
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)
|
||||||
@ -98,10 +99,29 @@ 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) -> dict:
|
def _serialize_project(project: Project, org_unit_name: str | None, report_count: int, years: list[InterruptedError] | None = None) -> dict:
|
||||||
return {
|
return {
|
||||||
"id": project.id,
|
"id": project.id,
|
||||||
"name": project.name,
|
"name": project.name,
|
||||||
@ -29,6 +29,7 @@ class ProjectRepository:
|
|||||||
"org_unit_id": project.org_unit_id,
|
"org_unit_id": project.org_unit_id,
|
||||||
"org_unit_name": org_unit_name,
|
"org_unit_name": org_unit_name,
|
||||||
"report_count": int(report_count),
|
"report_count": int(report_count),
|
||||||
|
"years": years,
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -66,11 +67,19 @@ class ProjectRepository:
|
|||||||
.scalar_subquery()
|
.scalar_subquery()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
report_years_subq = (
|
||||||
|
select(RfProjectReport.year)
|
||||||
|
.where(RfProjectReport.project_id == Project.id)
|
||||||
|
.distinct()
|
||||||
|
.scalar_subquery()
|
||||||
|
)
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
select(
|
select(
|
||||||
Project,
|
Project,
|
||||||
OrgUnit.title.label("org_unit_name"),
|
OrgUnit.title.label("org_unit_name"),
|
||||||
report_count_subq.label("report_count"),
|
report_count_subq.label("report_count"),
|
||||||
|
func.array(report_years_subq).label("years"),
|
||||||
)
|
)
|
||||||
.outerjoin(OrgUnit, OrgUnit.id == Project.org_unit_id)
|
.outerjoin(OrgUnit, OrgUnit.id == Project.org_unit_id)
|
||||||
.order_by(Project.id)
|
.order_by(Project.id)
|
||||||
@ -82,7 +91,7 @@ class ProjectRepository:
|
|||||||
query = query.limit(limit)
|
query = query.limit(limit)
|
||||||
|
|
||||||
rows = (await self.db.execute(query)).all()
|
rows = (await self.db.execute(query)).all()
|
||||||
payload = [self._serialize_project(row[0], row[1], row[2]) for row in rows]
|
payload = [self._serialize_project(row[0], row[1], row[2], row[3]) for row in rows]
|
||||||
if not with_count:
|
if not with_count:
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
@ -101,11 +110,18 @@ class ProjectRepository:
|
|||||||
.where(RfProjectReport.project_id == Project.id)
|
.where(RfProjectReport.project_id == Project.id)
|
||||||
.scalar_subquery()
|
.scalar_subquery()
|
||||||
)
|
)
|
||||||
|
report_years_subq = (
|
||||||
|
select(RfProjectReport.year)
|
||||||
|
.where(RfProjectReport.project_id == Project.id)
|
||||||
|
.distinct()
|
||||||
|
.scalar_subquery()
|
||||||
|
)
|
||||||
query = (
|
query = (
|
||||||
select(
|
select(
|
||||||
Project,
|
Project,
|
||||||
OrgUnit.title.label("org_unit_name"),
|
OrgUnit.title.label("org_unit_name"),
|
||||||
report_count_subq.label("report_count"),
|
report_count_subq.label("report_count"),
|
||||||
|
func.array(report_years_subq).label("years"),
|
||||||
)
|
)
|
||||||
.outerjoin(OrgUnit, OrgUnit.id == Project.org_unit_id)
|
.outerjoin(OrgUnit, OrgUnit.id == Project.org_unit_id)
|
||||||
.where(Project.id == project_id)
|
.where(Project.id == project_id)
|
||||||
@ -117,7 +133,24 @@ class ProjectRepository:
|
|||||||
row = (await self.db.execute(query)).first()
|
row = (await self.db.execute(query)).first()
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
return self._serialize_project(row[0], row[1], row[2])
|
return self._serialize_project(row[0], row[1], row[2], row[3])
|
||||||
|
|
||||||
|
async def get_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 = (
|
||||||
|
|||||||
39
api/src/repository/rf_project_report_line_repository.py
Normal file
39
api/src/repository/rf_project_report_line_repository.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.rf_project_report_line import RfProjectReportLine
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RfProjectReportLineRepository:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get(self, line_id: int) -> RfProjectReportLine | None:
|
||||||
|
query = select(
|
||||||
|
RfProjectReportLine
|
||||||
|
).where(
|
||||||
|
RfProjectReportLine.id == line_id
|
||||||
|
).limit(1)
|
||||||
|
return (await self.db.execute(query)).scalar_one_or_none()
|
||||||
|
|
||||||
|
async def get_list(self, line_ids: list[int]) -> list[RfProjectReportLine]:
|
||||||
|
query = select(
|
||||||
|
RfProjectReportLine
|
||||||
|
).where(
|
||||||
|
RfProjectReportLine.id.in_(line_ids)
|
||||||
|
)
|
||||||
|
return (await self.db.execute(query)).scalars().all()
|
||||||
|
|
||||||
|
async def get_ids(
|
||||||
|
self,
|
||||||
|
project_report_id: int,
|
||||||
|
) -> list[int]:
|
||||||
|
|
||||||
|
query = select(
|
||||||
|
RfProjectReportLine.id
|
||||||
|
).where(
|
||||||
|
RfProjectReportLine.rf_project_report_id == project_report_id
|
||||||
|
)
|
||||||
|
return (await self.db.execute(query)).scalars().all()
|
||||||
|
|
||||||
58
api/src/repository/rf_project_report_repository.py
Normal file
58
api/src/repository/rf_project_report_repository.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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,6 +161,59 @@ class SheetRepository:
|
|||||||
).all()
|
).all()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
async def add_program(
|
||||||
|
self,
|
||||||
|
form_id: int,
|
||||||
|
sheet: str,
|
||||||
|
name: str,
|
||||||
|
) -> list[tuple]:
|
||||||
|
return [
|
||||||
|
tuple(r) for r in (
|
||||||
|
await self.db.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT project_id, row_type, depth, sort_order, data
|
||||||
|
FROM v3.add_form4_program(
|
||||||
|
:p_name,
|
||||||
|
:p_form_id,
|
||||||
|
:p_sheet
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"p_form_id": form_id,
|
||||||
|
"p_name": name,
|
||||||
|
"p_sheet": sheet,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
]
|
||||||
|
|
||||||
|
async def add_project(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
program_id: int,
|
||||||
|
) -> list[tuple]:
|
||||||
|
return [
|
||||||
|
tuple(r) for r in (
|
||||||
|
await self.db.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT project_id, row_type, depth, sort_order, data
|
||||||
|
FROM v3.add_form4_project(
|
||||||
|
:p_name,
|
||||||
|
:p_program_id
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"p_program_id": program_id,
|
||||||
|
"p_name": name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
]
|
||||||
|
|
||||||
async def add_line(
|
async def add_line(
|
||||||
self,
|
self,
|
||||||
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,
|
||||||
location_form: str | None = None,
|
placement_type: str | None = None,
|
||||||
numbers_min: int | None = None,
|
staff_count_min: int | None = None,
|
||||||
numbers_max: int | None = None,
|
staff_count_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),
|
||||||
("location_form", location_form),
|
("placement_type", placement_type),
|
||||||
("open_date_start", open_date_start),
|
("open_date_start", open_date_start),
|
||||||
("open_date_end", open_date_end),
|
("open_date_end", open_date_end),
|
||||||
("numbers_min", numbers_min),
|
("staff_count_min", staff_count_min),
|
||||||
("numbers_max", numbers_max),
|
("staff_count_max", staff_count_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 ("location_form", str(value)) if value:
|
case ("placement_type", str(value)) if value:
|
||||||
queries.append(Vsp.location_form.ilike(f"%{value}%"))
|
queries.append(Vsp.placement_type.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 ("numbers_min", value) if value is not None:
|
case ("staff_count_min", value) if value is not None:
|
||||||
queries.append(Vsp.numbers >= value)
|
queries.append(Vsp.staff_count >= value)
|
||||||
case ("numbers_max", value) if value is not None:
|
case ("staff_count_max", value) if value is not None:
|
||||||
queries.append(Vsp.numbers <= value)
|
queries.append(Vsp.staff_count <= 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,6 +116,7 @@ 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("""
|
||||||
@ -124,8 +125,7 @@ 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_location_form, :p_numbers, :p_rent_contract_num,
|
:p_rent_contract_num, :p_rent_end_date
|
||||||
:p_rent_end_date
|
|
||||||
)
|
)
|
||||||
"""),
|
"""),
|
||||||
{
|
{
|
||||||
@ -144,8 +144,6 @@ 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"),
|
||||||
}
|
}
|
||||||
@ -176,9 +174,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",
|
||||||
@ -190,8 +188,7 @@ 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(
|
||||||
@ -199,8 +196,7 @@ 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_location_form, :p_numbers, :p_rent_contract_num,
|
:p_rent_contract_num, :p_rent_end_date
|
||||||
: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:
|
if not user or not user.is_active:
|
||||||
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:
|
if not user or not user.is_active:
|
||||||
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,6 +80,25 @@ 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,
|
||||||
@ -96,3 +115,20 @@ 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,5 +1,6 @@
|
|||||||
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
|
||||||
@ -10,7 +11,25 @@ class ExpenseItemService:
|
|||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
self.repo = ExpenseItemRepository(db)
|
self.repo = ExpenseItemRepository(db)
|
||||||
|
|
||||||
async def get(self, item_id: int, user: AppUser | None = None) -> ExpenseItem | None:
|
async def get(
|
||||||
|
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,6 +1185,238 @@ 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,6 +24,7 @@ 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,
|
||||||
)
|
)
|
||||||
@ -39,6 +40,17 @@ 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:
|
||||||
@ -87,6 +99,21 @@ 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,
|
||||||
@ -217,6 +244,9 @@ 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,
|
||||||
@ -229,7 +259,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))
|
worksheet = workbook.create_sheet(title=self._safe_sheet_name(plan.sheet_name, plan.effective_direction))
|
||||||
self._write_sheet_rows(
|
self._write_sheet_rows(
|
||||||
worksheet=worksheet,
|
worksheet=worksheet,
|
||||||
rows=rows,
|
rows=rows,
|
||||||
@ -404,7 +434,15 @@ 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) -> str:
|
def _safe_sheet_name(value: str, direction: str | None = None) -> 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, "_")
|
||||||
|
|||||||
106
api/src/services/form3_phase_service.py
Normal file
106
api/src/services/form3_phase_service.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.form3_phase import Form3Phase
|
||||||
|
from src.db.models.form_type import FormType, FormTypeEnum
|
||||||
|
from src.repository.form3_phase_repository import Form3PhaseRepository
|
||||||
|
from src.core.errors import AccessDeniedException, ValidationsError
|
||||||
|
from src.db.models.role import UserRoleEnum
|
||||||
|
from src.db.models.app_user import AppUser
|
||||||
|
from src.domain.schemas import Form3PhaseCreate, Form3PhaseUpdate
|
||||||
|
|
||||||
|
|
||||||
|
class Form3PhaseService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
self.f3p_repo = Form3PhaseRepository(db)
|
||||||
|
|
||||||
|
async def get_list(
|
||||||
|
self,
|
||||||
|
report_id: int | list[int],
|
||||||
|
phase_code: str | None = None,
|
||||||
|
) -> list[Form3Phase]:
|
||||||
|
return await self.f3p_repo.get_list(
|
||||||
|
report_id=report_id,
|
||||||
|
phase_code=phase_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self, report_id: int, phase_code: str, user: AppUser
|
||||||
|
) -> Form3Phase | None:
|
||||||
|
return await self.f3p_repo.get(
|
||||||
|
report_id=report_id,
|
||||||
|
phase_code=phase_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _validate_form3_phase(
|
||||||
|
self,
|
||||||
|
form_phase: Form3PhaseCreate | Form3PhaseUpdate,
|
||||||
|
) -> None:
|
||||||
|
errors = {}
|
||||||
|
if form_phase.column_keys:
|
||||||
|
form_type = FormType(code=FormTypeEnum.FORM_3)
|
||||||
|
for column_key in form_phase.column_keys:
|
||||||
|
section = column_key.split(".")
|
||||||
|
if section[0] not in form_type.section_list:
|
||||||
|
if "column_keys" in errors:
|
||||||
|
errors["column_keys"].append(f"Колонка {column_key} некорректна")
|
||||||
|
else:
|
||||||
|
errors["column_keys"] = [f"Колонка {column_key} некорректна"]
|
||||||
|
if errors:
|
||||||
|
raise ValidationsError(errors=errors)
|
||||||
|
|
||||||
|
|
||||||
|
async def create(
|
||||||
|
self,
|
||||||
|
report_id: int,
|
||||||
|
body: Form3PhaseCreate,
|
||||||
|
user: AppUser,
|
||||||
|
) -> Form3Phase:
|
||||||
|
if user.role_id != UserRoleEnum.ADMIN:
|
||||||
|
raise AccessDeniedException
|
||||||
|
|
||||||
|
self._validate_form3_phase(
|
||||||
|
form_phase=body,
|
||||||
|
)
|
||||||
|
return await self.f3p_repo.create(
|
||||||
|
report_id=report_id,
|
||||||
|
phase_code=body.phase_code,
|
||||||
|
role=body.role,
|
||||||
|
column_keys=body.column_keys,
|
||||||
|
opens_at=body.opens_at,
|
||||||
|
closes_at=body.closes_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def update(
|
||||||
|
self,
|
||||||
|
report_id: int,
|
||||||
|
phase_code: str,
|
||||||
|
body: Form3PhaseUpdate,
|
||||||
|
user: AppUser,
|
||||||
|
) -> Form3Phase:
|
||||||
|
if user.role_id != UserRoleEnum.ADMIN:
|
||||||
|
raise AccessDeniedException
|
||||||
|
|
||||||
|
self._validate_form3_phase(
|
||||||
|
form_phase=body,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = body.model_dump(exclude_unset=True)
|
||||||
|
if not data:
|
||||||
|
return await self.f3p_repo.get(
|
||||||
|
report_id=report_id,
|
||||||
|
phase_code=phase_code,
|
||||||
|
)
|
||||||
|
return await self.f3p_repo.update(
|
||||||
|
report_id=report_id,
|
||||||
|
phase_code=phase_code,
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def delete(
|
||||||
|
self, report_id: int, phase_code: str, user: AppUser,
|
||||||
|
) -> bool:
|
||||||
|
return await self.f3p_repo.delete(
|
||||||
|
report_id=report_id,
|
||||||
|
phase_code=phase_code,
|
||||||
|
)
|
||||||
@ -54,6 +54,7 @@ class OrgUnitService:
|
|||||||
self,
|
self,
|
||||||
title: str,
|
title: str,
|
||||||
is_ssp: bool,
|
is_ssp: bool,
|
||||||
|
utc_offset: str,
|
||||||
user: AppUser,
|
user: AppUser,
|
||||||
) -> OrgUnit:
|
) -> OrgUnit:
|
||||||
"""Создание нового ССП/РФ."""
|
"""Создание нового ССП/РФ."""
|
||||||
@ -63,6 +64,7 @@ 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,5 +1,6 @@
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.project import Project
|
||||||
from src.db.models.app_user import AppUser
|
from src.db.models.app_user import AppUser
|
||||||
from src.db.models.role import UserRoleEnum
|
from src.db.models.role import UserRoleEnum
|
||||||
from src.core.errors import AccessDeniedException, ValidationException
|
from src.core.errors import AccessDeniedException, ValidationException
|
||||||
@ -41,6 +42,10 @@ class ProjectService:
|
|||||||
project["reports"] = await self.project_repo.get_reports(project_id=project_id)
|
project["reports"] = await self.project_repo.get_reports(project_id=project_id)
|
||||||
return project
|
return project
|
||||||
|
|
||||||
|
async def get_instance(self, project_id: int, user: AppUser) -> Project | None:
|
||||||
|
org_unit_ids = await self._allowed_org_unit_ids(user)
|
||||||
|
return await self.project_repo.get_instance(project_id=project_id, org_unit_ids=org_unit_ids)
|
||||||
|
|
||||||
async def resolve_report_id(
|
async def resolve_report_id(
|
||||||
self,
|
self,
|
||||||
project_id: int,
|
project_id: int,
|
||||||
|
|||||||
34
api/src/services/rf_project_report_line_service.py
Normal file
34
api/src/services/rf_project_report_line_service.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.rf_project_report_line import RfProjectReportLine
|
||||||
|
from src.repository.rf_project_report_line_repository import RfProjectReportLineRepository
|
||||||
|
from src.db.models.app_user import AppUser
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RfProjectReportLineService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
self.prl_repo = RfProjectReportLineRepository(db)
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self,
|
||||||
|
user: AppUser,
|
||||||
|
line_id: int,
|
||||||
|
) -> RfProjectReportLine | None:
|
||||||
|
return await self.prl_repo.get(line_id=line_id)
|
||||||
|
|
||||||
|
async def get_list(
|
||||||
|
self,
|
||||||
|
user: AppUser,
|
||||||
|
line_ids: list[int],
|
||||||
|
) -> list[RfProjectReportLine]:
|
||||||
|
return await self.prl_repo.get_list(line_ids=line_ids)
|
||||||
|
|
||||||
|
async def get_ids(
|
||||||
|
self,
|
||||||
|
project_report_id: int,
|
||||||
|
) -> list[int]:
|
||||||
|
return await self.prl_repo.get_ids(project_report_id=project_report_id)
|
||||||
45
api/src/services/rf_project_report_service.py
Normal file
45
api/src/services/rf_project_report_service.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
|
||||||
|
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,9 +1,14 @@
|
|||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.form4_project import Form4ProjectLevelEnum
|
||||||
|
from src.db.models.role import UserRoleEnum
|
||||||
|
from src.repository.budget_form_repository import BudgetFormRepository
|
||||||
|
from src.repository.form4_project_repository import Form4ProjectRepository
|
||||||
|
from src.repository.user_repository import UserRepository
|
||||||
from src.db.models.app_user import AppUser
|
from src.db.models.app_user import AppUser
|
||||||
from src.db.models.budget_form import BudgetForm
|
from src.db.models.budget_form import BudgetForm
|
||||||
from src.repository.sheet_repository import SheetRepository
|
from src.repository.sheet_repository import SheetRepository, SheetValidationError
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -11,6 +16,9 @@ class SheetService:
|
|||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
self.db = db
|
self.db = db
|
||||||
self.sheet_repo = SheetRepository(db)
|
self.sheet_repo = SheetRepository(db)
|
||||||
|
self.bf_repo = BudgetFormRepository(db)
|
||||||
|
self.user_repo = UserRepository(db)
|
||||||
|
self.f4_project_repo = Form4ProjectRepository(db)
|
||||||
|
|
||||||
async def get(
|
async def get(
|
||||||
self,
|
self,
|
||||||
@ -27,28 +35,6 @@ 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,
|
||||||
@ -124,6 +110,66 @@ 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,6 +11,7 @@ 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
|
||||||
|
|
||||||
@ -20,8 +21,8 @@ _info_close_sync_lock = asyncio.Lock()
|
|||||||
|
|
||||||
class VSPService:
|
class VSPService:
|
||||||
EXECUTOR_EDITABLE_FIELDS = {
|
EXECUTOR_EDITABLE_FIELDS = {
|
||||||
"location_form",
|
"placement_type",
|
||||||
"numbers",
|
"staff_count",
|
||||||
"total_area",
|
"total_area",
|
||||||
"rent_contract_num",
|
"rent_contract_num",
|
||||||
"rent_end_date",
|
"rent_end_date",
|
||||||
@ -31,6 +32,7 @@ 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]:
|
||||||
@ -54,9 +56,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,
|
||||||
location_form: str | None = None,
|
placement_type: str | None = None,
|
||||||
numbers_min: int | None = None,
|
staff_count_min: int | None = None,
|
||||||
numbers_max: int | None = None,
|
staff_count_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,
|
||||||
@ -84,9 +86,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,
|
||||||
location_form=location_form,
|
placement_type=placement_type,
|
||||||
numbers_min=numbers_min,
|
staff_count_min=staff_count_min,
|
||||||
numbers_max=numbers_max,
|
staff_count_max=staff_count_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,
|
||||||
@ -118,16 +120,17 @@ 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():
|
||||||
@ -144,6 +147,8 @@ 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(
|
||||||
@ -154,9 +159,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,
|
||||||
location_form: str | None = None,
|
placement_type: str | None = None,
|
||||||
numbers_min: int | None = None,
|
staff_count_min: int | None = None,
|
||||||
numbers_max: int | None = None,
|
staff_count_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,
|
||||||
@ -165,9 +170,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,
|
||||||
location_form=location_form,
|
placement_type=placement_type,
|
||||||
numbers_min=numbers_min,
|
staff_count_min=staff_count_min,
|
||||||
numbers_max=numbers_max,
|
staff_count_max=staff_count_max,
|
||||||
is_active=None,
|
is_active=None,
|
||||||
load_org_unit=True,
|
load_org_unit=True,
|
||||||
)
|
)
|
||||||
@ -188,8 +193,8 @@ class VSPService:
|
|||||||
"open_date",
|
"open_date",
|
||||||
"close_date",
|
"close_date",
|
||||||
"notes",
|
"notes",
|
||||||
"location_form",
|
"placement_type",
|
||||||
"numbers",
|
"staff_count",
|
||||||
"area",
|
"area",
|
||||||
"rent_contract_num",
|
"rent_contract_num",
|
||||||
"rent_end_date",
|
"rent_end_date",
|
||||||
@ -215,8 +220,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.location_form or None,
|
item.placement_type or None,
|
||||||
item.numbers if item.numbers is not None else None,
|
item.staff_count if item.staff_count 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,
|
||||||
@ -245,6 +250,9 @@ 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):
|
||||||
@ -266,6 +274,8 @@ 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:
|
||||||
@ -291,3 +301,10 @@ 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,3 +30,28 @@ 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": 1,
|
"budget_form_id": 4,
|
||||||
"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:00Z",
|
"opens_at": "2026-01-01T00:00:00",
|
||||||
"closes_at": "2026-12-31T00:00:00Z",
|
"closes_at": "2026-12-31T00:00:00",
|
||||||
}
|
}
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/stages/form/1",
|
"/api/v1/stages/form/4",
|
||||||
json=body,
|
json=body,
|
||||||
headers=_auth_headers(admin_tokens),
|
headers=_auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
@ -102,22 +102,31 @@ 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
|
||||||
assert isinstance(payload["result"], dict)
|
result = payload["result"]
|
||||||
|
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": 1,
|
"budget_form_id": 4,
|
||||||
"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:00Z",
|
"opens_at": "2026-01-01T00:00:00",
|
||||||
"closes_at": "2026-12-31T00:00:00Z",
|
"closes_at": "2026-12-31T00:00:00",
|
||||||
}
|
}
|
||||||
response = client.patch(
|
response = client.patch(
|
||||||
"/api/v1/stages/form/1/AHR/test",
|
"/api/v1/stages/form/4/AHR/test",
|
||||||
json=body,
|
json=body,
|
||||||
headers=_auth_headers(admin_tokens),
|
headers=_auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
@ -127,8 +136,15 @@ 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
|
||||||
assert isinstance(payload["result"], dict)
|
result = payload["result"]
|
||||||
assert payload["result"] == body
|
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
|
||||||
|
|
||||||
|
|
||||||
def test_form_phases_delete(client, admin_tokens):
|
def test_form_phases_delete(client, admin_tokens):
|
||||||
@ -138,3 +154,165 @@ def test_form_phases_delete(client, admin_tokens):
|
|||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_form3_phases_list_smoke(client, admin_tokens):
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/stages/project/2", headers=_auth_headers(admin_tokens)
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert "result" in payload
|
||||||
|
assert "count" in payload
|
||||||
|
assert isinstance(payload["result"], list)
|
||||||
|
|
||||||
|
|
||||||
|
def test_form3_phases_list_filter_smoke(client, admin_tokens):
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/stages/project/2", headers=_auth_headers(admin_tokens)
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert "count" in payload
|
||||||
|
count = payload["count"]
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/stages/project/2?&phase_code=test", headers=_auth_headers(admin_tokens)
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert "count" in payload
|
||||||
|
assert payload["count"] <= count
|
||||||
|
|
||||||
|
|
||||||
|
def test_form3_phases_create_forbidden(client, isp_tokens):
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/stages/project/2",
|
||||||
|
json={
|
||||||
|
"year": 2026,
|
||||||
|
"report_type": "LIMIT",
|
||||||
|
"phase_code": "CODE1",
|
||||||
|
"role": "DFIP",
|
||||||
|
"column_keys": [
|
||||||
|
"q1.adj_by_items",
|
||||||
|
"q1.adj_increase"
|
||||||
|
],
|
||||||
|
"opens_at": "2027-07-20T00:00:00",
|
||||||
|
"closes_at": "2027-09-21T00:00:00"
|
||||||
|
},
|
||||||
|
headers=_auth_headers(isp_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
payload = response.json()
|
||||||
|
assert isinstance(payload, dict)
|
||||||
|
assert "message" in payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_form3_phases_update_forbidden(client, isp_tokens):
|
||||||
|
response = client.patch(
|
||||||
|
"/api/v1/stages/project/2/2026/LIMIT/RF_FILL_2026",
|
||||||
|
json={
|
||||||
|
"role": "DFIP",
|
||||||
|
"column_keys": [
|
||||||
|
"q1.adj_by_items",
|
||||||
|
"q1.adj_increase"
|
||||||
|
],
|
||||||
|
"opens_at": "2027-07-20T00:00:00",
|
||||||
|
"closes_at": "2027-09-21T00:00:00"
|
||||||
|
},
|
||||||
|
headers=_auth_headers(isp_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
payload = response.json()
|
||||||
|
assert isinstance(payload, dict)
|
||||||
|
assert "message" in payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_form3_phases_delete_forbidden(client, isp_tokens):
|
||||||
|
response = client.delete(
|
||||||
|
"/api/v1/stages/project/2/2026/LIMIT/RF_FILL_2026",
|
||||||
|
headers=_auth_headers(isp_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
payload = response.json()
|
||||||
|
assert isinstance(payload, dict)
|
||||||
|
assert "message" in payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_form3_phases_create(client, admin_tokens):
|
||||||
|
body = {
|
||||||
|
"year": 2026,
|
||||||
|
"report_type": "LIMIT",
|
||||||
|
"phase_code": "CODE1",
|
||||||
|
"role": "DFIP",
|
||||||
|
"column_keys": [
|
||||||
|
"q1.adj_by_items",
|
||||||
|
"q1.adj_increase"
|
||||||
|
],
|
||||||
|
"opens_at": "2027-07-20T00:00:00",
|
||||||
|
"closes_at": "2027-09-21T00:00:00"
|
||||||
|
}
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/stages/project/2",
|
||||||
|
json=body,
|
||||||
|
headers=_auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert "success" in payload
|
||||||
|
assert payload["success"]
|
||||||
|
assert "result" in payload
|
||||||
|
result = payload["result"]
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
opens_at = result.pop("opens_at", None)
|
||||||
|
assert "+03" in opens_at
|
||||||
|
assert body.pop("opens_at") == opens_at.split("+")[0]
|
||||||
|
closes_at = result.pop("closes_at", None)
|
||||||
|
assert "+03" in closes_at
|
||||||
|
assert body.pop("closes_at") == closes_at.split("+")[0]
|
||||||
|
assert result == body
|
||||||
|
|
||||||
|
assert payload["result"] == body
|
||||||
|
|
||||||
|
|
||||||
|
def test_form3_phases_update(client, admin_tokens):
|
||||||
|
body = {
|
||||||
|
"role": "DFIP",
|
||||||
|
"column_keys": [
|
||||||
|
"q1.adj_by_items",
|
||||||
|
"q1.adj_increase"
|
||||||
|
],
|
||||||
|
"opens_at": "2027-07-20T00:00:00",
|
||||||
|
"closes_at": "2027-09-21T00:00:00"
|
||||||
|
}
|
||||||
|
response = client.patch(
|
||||||
|
"/api/v1/stages/project/2/2026/LIMIT/RF_FILL_2026",
|
||||||
|
json=body,
|
||||||
|
headers=_auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
|
||||||
|
assert "success" in payload
|
||||||
|
assert payload["success"]
|
||||||
|
assert "result" in payload
|
||||||
|
result = payload["result"]
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
opens_at = result.pop("opens_at", None)
|
||||||
|
assert "+03" in opens_at
|
||||||
|
assert body.pop("opens_at") == opens_at.split("+")[0]
|
||||||
|
closes_at = result.pop("closes_at", None)
|
||||||
|
assert "+03" in closes_at
|
||||||
|
assert body.pop("closes_at") == closes_at.split("+")[0]
|
||||||
|
|
||||||
|
body.update({'phase_code': 'RF_FILL_2026', 'report_type': 'LIMIT', 'year': 2026})
|
||||||
|
assert result == body
|
||||||
|
|
||||||
|
|
||||||
|
def test_form3_phases_delete(client, admin_tokens):
|
||||||
|
response = client.delete(
|
||||||
|
"/api/v1/stages/project/2/2026/LIMIT/RF_FILL_2026",
|
||||||
|
headers=_auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|||||||
@ -161,6 +161,25 @@ 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,6 +78,89 @@ 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,6 +48,31 @@ 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)], 1),
|
([("ssp_id", 2)], 2),
|
||||||
([("form_id", 1)], 0),
|
([("form_id", 1)], 0),
|
||||||
([("form_id", 2)], 1),
|
([("form_id", 2)], 2),
|
||||||
([("ssp_id", 2), ("form_id", 1)], 1),
|
([("ssp_id", 2), ("form_id", 1)], 2),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
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": "Тестовое примечание",
|
||||||
"location_form": "Встроенное",
|
"placement_type": "Аренда",
|
||||||
"numbers": 10,
|
"staff_count": 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"]["location_form"] == "Встроенное"
|
assert payload["result"]["placement_type"] == "Аренда"
|
||||||
assert payload["result"]["numbers"] == 10
|
assert payload["result"]["staff_count"] == 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,6 +150,36 @@ 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(
|
||||||
@ -195,7 +225,33 @@ 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": 1,
|
"ssp_id": 2,
|
||||||
|
},
|
||||||
|
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) VALUES
|
INSERT INTO v3.org_unit (id,title,is_active,is_ssp,utc_offset) VALUES
|
||||||
(1,'Test_SSP',true,true),
|
(1,'Test_SSP',true,true,'+03'),
|
||||||
(2,'Test РФ',true,false);
|
(2,'Test РФ',true,false,'+05');
|
||||||
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,9 +24,24 @@ 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,location_form,numbers,rent_contract_num,rent_end_date) VALUES
|
INSERT INTO v3.vsp (id,branch_id,reg_number,address,format,opened_at,placement_type,staff_count,total_area,closed_at,is_active,updated_at,is_deleted,created_at,created_by,system_code,updated_by,vsp_type,notes,rent_contract_num,rent_end_date) VALUES
|
||||||
(1,2,'Тестовый всп 1','Тестовая 12','укукк','2026-05-07','ывс',2026,230,'2026-06-16',false,'2026-06-18 15:46:38.611903',false,'2026-06-04 14:36:59.737727',1,'1233',91,'','','встроенное помещение',NULL,'',NULL),
|
(1,2,'Тестовый всп 1','Тестовая 12','укукк','2026-05-07','ывс',2026,230,'2026-06-16',false,'2026-06-18 15:46:38.611903',false,'2026-06-04 14:36:59.737727',1,'1233',91,'','','',NULL),
|
||||||
(2,2,'Test12 -----','Тестовая 125','формат 15 ','2026-06-08',NULL,NULL,58,NULL,true,'2026-06-16 18:53:17.986651',false,'2026-06-09 09:04:33.29858',91,'123-1234',91,'субаренда','Работает ','аренда',155,'12-45','2026-10-02');
|
(2,2,'Test12 -----','Тестовая 125','формат 15 ','2026-06-08',NULL,NULL,58,NULL,true,'2026-06-16 18:53:17.986651',false,'2026-06-09 09:04:33.29858',91,'123-1234',91,'субаренда','Работает ','12-45','2027-10-02'),
|
||||||
SELECT setval('v3.vsp_id_seq', 2);
|
(3,2,'3434','победы 12','формат','2026-06-01','субаренда', NULL,34,NULL,true,'2026-07-01 13:32:35.327097',false,'2026-06-29 15:53:50.236062',91,'3434',NULL,'2','примечание','567','2027-07-01');
|
||||||
|
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');
|
||||||
|
|||||||
@ -22,6 +22,6 @@ export const DictVspApi = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
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);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
34
web/src/api/projects.js
Normal file
34
web/src/api/projects.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
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,6 +17,7 @@ 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();
|
||||||
@ -37,8 +38,10 @@ 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 />} />
|
||||||
@ -49,7 +52,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" element={<NewFormTablePage />} />
|
<Route path="/table/form/:formId/form-type/:formType/:sheetName/:direction/:year" 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';
|
import { ROLES_NAME_ID } from '../../constants/constants';
|
||||||
|
|
||||||
const Switch = styled.div`
|
const Switch = styled.div`
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
|
|||||||
@ -1,116 +1,8 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { memo, useMemo, useCallback } 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`
|
// Константы вне компонента
|
||||||
display: -webkit-box;
|
const BASE_CELL_STYLES = {
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Стилизованный компонент для плашки блокировки
|
|
||||||
const LockBadge = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
right: 2px;
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
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 formatNumber = (value) => {
|
|
||||||
if (value === null || value === undefined || value === '') return String(value || '');
|
|
||||||
|
|
||||||
const num = Number(value);
|
|
||||||
if (isNaN(num)) return String(value);
|
|
||||||
|
|
||||||
return num.toLocaleString('ru-RU', {
|
|
||||||
minimumFractionDigits: 1,
|
|
||||||
maximumFractionDigits: 1,
|
|
||||||
useGrouping: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundColor, color, isEditable }) => {
|
|
||||||
const { lockedCells } = useRealtime();
|
|
||||||
const cellKey = `${row.id}_${column.id}`;
|
|
||||||
|
|
||||||
const isLocked = lockedCells.includes(cellKey);
|
|
||||||
|
|
||||||
const cellValue = cell.getValue();
|
|
||||||
let textValue = String(cellValue || '');
|
|
||||||
|
|
||||||
const isNumeric = !isNaN(Number(cellValue)) && cellValue !== null && cellValue !== undefined && cellValue !== '';
|
|
||||||
if (isNumeric) {
|
|
||||||
textValue = formatNumber(cellValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
const highlightedContent = useMemo(() => {
|
|
||||||
return highlightText(textValue, globalFilter);
|
|
||||||
}, [textValue, globalFilter]);
|
|
||||||
|
|
||||||
// Базовые стили для заблокированной ячейки
|
|
||||||
const lockedStyles = isLocked ? {
|
|
||||||
border: '2px solid #e0e0e0',
|
|
||||||
backgroundColor: '#f5f5f5',
|
|
||||||
color: '#999999',
|
|
||||||
cursor: 'not-allowed',
|
|
||||||
opacity: 0.85,
|
|
||||||
} : {};
|
|
||||||
|
|
||||||
// Если isEditable false, добавляем дополнительные стили
|
|
||||||
const editableStyles = !isEditable ? {
|
|
||||||
border: '2px solid #d3d3d3',
|
|
||||||
color: backgroundColor?.toLowerCase() === '#933634' ? '#ffffff' : '#525252',
|
|
||||||
cursor: 'not-allowed',
|
|
||||||
} : {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="cell"
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -124,21 +16,190 @@ const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundC
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
border: '2px solid transparent',
|
border: '2px solid transparent',
|
||||||
padding: '8px',
|
padding: '8px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const LOCKED_STYLES = {
|
||||||
|
border: '2px solid #e0e0e0',
|
||||||
|
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 || '');
|
||||||
|
const num = Number(value);
|
||||||
|
if (isNaN(num)) return String(value);
|
||||||
|
return num.toLocaleString('ru-RU', {
|
||||||
|
minimumFractionDigits: asInteger ? 0 : 1,
|
||||||
|
maximumFractionDigits: asInteger ? 0 : 1,
|
||||||
|
useGrouping: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Оптимизированная функция подсветки
|
||||||
|
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 cellKey = `${row.id}_${column.id}`;
|
||||||
|
const isLocked = lockedCells.includes(cellKey);
|
||||||
|
|
||||||
|
// Мемоизация значения
|
||||||
|
const { rawValue, displayValue, isNumeric } = useMemo(() => {
|
||||||
|
const value = cell.getValue();
|
||||||
|
const isNum = !isNaN(Number(value)) && value !== null && value !== undefined && value !== '';
|
||||||
|
let display = String(value || '');
|
||||||
|
|
||||||
|
if (isNum) {
|
||||||
|
const asInteger = INTEGER_COLUMN_IDS.has(column.id);
|
||||||
|
display = formatNumber(value, asInteger);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { rawValue: value, displayValue: display, isNumeric: isNum };
|
||||||
|
}, [cell, column.id]);
|
||||||
|
|
||||||
|
const highlightedContent = useMemo(() => {
|
||||||
|
const searchQueries = [globalFilter, columnFilter];
|
||||||
|
return createHighlightedContent(rawValue, displayValue, searchQueries);
|
||||||
|
}, [rawValue, displayValue, globalFilter, columnFilter]);
|
||||||
|
|
||||||
|
const textColor = useMemo(() => {
|
||||||
|
if (isLocked) return '#999999';
|
||||||
|
const brightness = getColorBrightness(backgroundColor);
|
||||||
|
return brightness < 128 ? '#ffffff' : '#000000';
|
||||||
|
}, [backgroundColor, isLocked]);
|
||||||
|
|
||||||
|
const cellStyles = useMemo(() => ({
|
||||||
|
...BASE_CELL_STYLES,
|
||||||
backgroundColor: isLocked ? '#f5f5f5' : backgroundColor,
|
backgroundColor: isLocked ? '#f5f5f5' : backgroundColor,
|
||||||
color: isLocked ? '#999999' : color,
|
color: textColor,
|
||||||
...lockedStyles,
|
...(isLocked ? LOCKED_STYLES : {}),
|
||||||
...editableStyles,
|
}), [backgroundColor, isLocked, textColor]);
|
||||||
}}
|
|
||||||
onClick={isLocked ? undefined : onClick}
|
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 (
|
||||||
|
<div
|
||||||
|
className="cell"
|
||||||
|
style={cellStyles}
|
||||||
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<ContainerForText>{highlightedContent}</ContainerForText>
|
<span style={CONTAINER_STYLES}>{highlightedContent}</span>
|
||||||
{isLocked && (
|
{lockBadge}
|
||||||
<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,129 +1,15 @@
|
|||||||
// EditCell.js - обновленная версия
|
import React, { memo, useEffect, useRef, useLayoutEffect, useState, useCallback, useMemo } from 'react';
|
||||||
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 EditCell = ({ refCell, onChange, disabled, value, tableId, cellId }) => {
|
const EDITOR_STYLES = {
|
||||||
const [curValue, setCurValue] = useState(value);
|
|
||||||
const [displayValue, setDisplayValue] = useState(value);
|
|
||||||
const [isFormulaMode, setIsFormulaMode] = useState(false);
|
|
||||||
|
|
||||||
// Ключ для хранения формул в localStorage
|
|
||||||
const storageKey = `formula_${tableId}_${cellId}`;
|
|
||||||
|
|
||||||
const trFrag = refCell.current.offsetParent.offsetParent;
|
|
||||||
const tdFrag = refCell.current.offsetParent;
|
|
||||||
const left = tdFrag.offsetLeft;
|
|
||||||
const width = refCell.current.offsetParent.offsetWidth;
|
|
||||||
const translateY = trFrag.style.transform;
|
|
||||||
|
|
||||||
// Загружаем сохраненную формулу при монтировании
|
|
||||||
useEffect(() => {
|
|
||||||
const savedFormula = loadFromStorage(storageKey);
|
|
||||||
if (savedFormula && isFormula(savedFormula)) {
|
|
||||||
setIsFormulaMode(true);
|
|
||||||
setCurValue(savedFormula);
|
|
||||||
// Вычисляем и отображаем результат
|
|
||||||
try {
|
|
||||||
const formula = extractFormula(savedFormula);
|
|
||||||
const result = calculateFormula(formula);
|
|
||||||
setDisplayValue(formatNumber(result));
|
|
||||||
} catch (error) {
|
|
||||||
setDisplayValue('#ОШИБКА');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setDisplayValue(value);
|
|
||||||
}
|
|
||||||
}, [storageKey, value]);
|
|
||||||
|
|
||||||
function useAutoResize(value) {
|
|
||||||
const ref = useRef(null);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const el = ref.current;
|
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
el.style.height = 'auto';
|
|
||||||
el.style.minHeight = '3rem';
|
|
||||||
el.style.height = el.scrollHeight + 'px';
|
|
||||||
el.style.maxHeight = '180px';
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
return ref;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ref = useAutoResize(value);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (ref.current && !disabled) {
|
|
||||||
ref.current.focus();
|
|
||||||
ref.current.setSelectionRange(
|
|
||||||
ref.current.value.length,
|
|
||||||
ref.current.value.length,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [disabled, ref]);
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
// Проверяем, является ли введенное значение формулой
|
|
||||||
if (isFormula(curValue)) {
|
|
||||||
// Сохраняем формулу в localStorage
|
|
||||||
saveToStorage(storageKey, curValue);
|
|
||||||
|
|
||||||
// Вычисляем и сохраняем результат
|
|
||||||
try {
|
|
||||||
const formula = extractFormula(curValue);
|
|
||||||
const result = calculateFormula(formula);
|
|
||||||
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 (
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div
|
|
||||||
key="blockForEdit"
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: left,
|
|
||||||
zIndex: 12,
|
zIndex: 12,
|
||||||
width: width,
|
|
||||||
transform: translateY,
|
|
||||||
top: 0,
|
top: 0,
|
||||||
}}
|
};
|
||||||
>
|
|
||||||
{isFormulaMode && (
|
const FORMULA_INDICATOR_STYLES = {
|
||||||
<div style={{
|
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '-20px',
|
top: '-20px',
|
||||||
left: '4px',
|
left: '4px',
|
||||||
@ -132,42 +18,200 @@ const EditCell = ({ refCell, onChange, disabled, value, tableId, cellId }) => {
|
|||||||
background: '#f0f0f0',
|
background: '#f0f0f0',
|
||||||
padding: '2px 6px',
|
padding: '2px 6px',
|
||||||
borderRadius: '3px',
|
borderRadius: '3px',
|
||||||
pointerEvents: 'none'
|
pointerEvents: 'none',
|
||||||
}}>
|
};
|
||||||
fx
|
|
||||||
</div>
|
const TEXTAREA_STYLES = {
|
||||||
)}
|
|
||||||
<textarea
|
|
||||||
ref={ref}
|
|
||||||
disabled={disabled}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: 'auto',
|
height: '3rem',
|
||||||
fieldSizing: 'content',
|
fieldSizing: 'content',
|
||||||
fontSize: 'inherit',
|
fontSize: 'inherit',
|
||||||
backgroundColor: isFormulaMode ? '#f8f9fa' : 'white',
|
};
|
||||||
border: isFormulaMode ? '2px solid #4a90d9' : undefined,
|
|
||||||
}}
|
const EditCell = memo(({ refCell, onChange, disabled, value, tableId, cellId, table }) => {
|
||||||
onChange={handleChangeValue}
|
const { endEditing: contextEndEditing } = useRealtime();
|
||||||
value={curValue}
|
const storageKey = `formula_${tableId}_${cellId}`;
|
||||||
onBlur={handleSave}
|
|
||||||
onKeyDown={(e) => {
|
const [state, setState] = useState(() => ({
|
||||||
|
currentValue: value,
|
||||||
|
displayValue: value,
|
||||||
|
isFormulaMode: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Позиция ячейки
|
||||||
|
const [position, setPosition] = useState();
|
||||||
|
const textareaRef = useRef(null);
|
||||||
|
|
||||||
|
const refFinished = useRef(false);
|
||||||
|
|
||||||
|
// Загрузка сохраненной формулы
|
||||||
|
useEffect(() => {
|
||||||
|
const savedFormula = loadFromStorage(storageKey);
|
||||||
|
if (savedFormula && isFormula(savedFormula)) {
|
||||||
|
try {
|
||||||
|
const formula = extractFormula(savedFormula);
|
||||||
|
const result = calculateFormula(formula);
|
||||||
|
setState({
|
||||||
|
currentValue: savedFormula,
|
||||||
|
displayValue: formatNumber(result),
|
||||||
|
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 {
|
||||||
|
localStorage.removeItem(storageKey);
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
displayValue: currentValue,
|
||||||
|
isFormulaMode: false,
|
||||||
|
}));
|
||||||
|
onChange?.(currentValue);
|
||||||
|
}
|
||||||
|
}, [state, storageKey, onChange]);
|
||||||
|
|
||||||
|
const finishEditing = useCallback(() => {
|
||||||
|
if (refFinished.current) return;
|
||||||
|
refFinished.current = true;
|
||||||
|
if (!disabled) {
|
||||||
|
handleSave();
|
||||||
|
}
|
||||||
|
const editingCell = table.getState().editingCell;
|
||||||
|
if (editingCell) {
|
||||||
|
contextEndEditing?.(editingCell.row, editingCell.column);
|
||||||
|
}
|
||||||
|
}, [disabled, handleSave, table, contextEndEditing]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
const el = textareaRef.current;
|
||||||
|
if (el && !el.contains(event.target)) {
|
||||||
|
finishEditing();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSave();
|
finishEditing();
|
||||||
// Закрываем редактирование
|
|
||||||
if (onChange) {
|
|
||||||
// Симулируем завершение редактирования
|
|
||||||
document.activeElement?.blur();
|
|
||||||
}
|
}
|
||||||
}
|
}, [finishEditing]);
|
||||||
}}
|
|
||||||
placeholder={isFormulaMode ? 'Введите формулу (начинается с =)' : ''}
|
const editorStyles = useMemo(() => ({
|
||||||
|
...EDITOR_STYLES,
|
||||||
|
left: position?.left || 0,
|
||||||
|
width: position?.width || 0,
|
||||||
|
transform: position?.translateY || 0,
|
||||||
|
}), [position]);
|
||||||
|
|
||||||
|
const textareaStyles = useMemo(() => ({
|
||||||
|
...TEXTAREA_STYLES,
|
||||||
|
backgroundColor: state.isFormulaMode ? '#f8f9fa' : 'white',
|
||||||
|
border: state.isFormulaMode ? '2px solid #4a90d9' : undefined,
|
||||||
|
}), [state.isFormulaMode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{position &&
|
||||||
|
<div style={editorStyles}>
|
||||||
|
{state.isFormulaMode && (
|
||||||
|
<div style={FORMULA_INDICATOR_STYLES}>fx</div>
|
||||||
|
)}
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
disabled={disabled}
|
||||||
|
style={textareaStyles}
|
||||||
|
onChange={handleChange}
|
||||||
|
value={state.currentValue}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={state.isFormulaMode ? 'Введите формулу (начинается с =)' : ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
EditCell.displayName = 'EditCell';
|
||||||
|
|
||||||
export default EditCell;
|
export default EditCell;
|
||||||
@ -0,0 +1,148 @@
|
|||||||
|
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,4 +1,3 @@
|
|||||||
// 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 {
|
||||||
@ -11,13 +10,14 @@ 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,
|
||||||
getTableHeadCellStyles,
|
getTableBodyCellProps,
|
||||||
getTablePaperStyles,
|
getTablePaperStyles,
|
||||||
TABLE_ROW_HEIGHT,
|
TABLE_ROW_HEIGHT,
|
||||||
} from './constants/tableConfig';
|
} from './constants/tableConfig';
|
||||||
@ -25,14 +25,15 @@ 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 }) => {
|
const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||||
const { data, setData, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction);
|
const { data, setData, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year);
|
||||||
const [globalFilter, setGlobalFilter] = useState('');
|
const [globalFilter, setGlobalFilter] = useState('');
|
||||||
const [showColumnFilters, setShowColumnFilters] = useState(false);
|
const [showColumnFilters, setShowColumnFilters] = useState(false);
|
||||||
const [selectedColumn, setSelectedColumn] = useState();
|
const [selectedColumnId, setSelectedColumnId] = 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();
|
||||||
@ -46,11 +47,14 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
return additionVspRowTable[formType].includes(sheetName);
|
return additionVspRowTable[formType].includes(sheetName);
|
||||||
}, [formType, sheetName]);
|
}, [formType, sheetName]);
|
||||||
|
|
||||||
const handleGlobalFilterChange = (value) => {
|
const handleGlobalFilterChange = useCallback(
|
||||||
|
debounce((value) => {
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
setGlobalFilter(value);
|
setGlobalFilter(value);
|
||||||
});
|
});
|
||||||
};
|
}, 300),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isConnected,
|
isConnected,
|
||||||
@ -62,7 +66,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
addRow: contextAddRow,
|
addRow: contextAddRow,
|
||||||
deleteRow: contextDeleteRow,
|
deleteRow: contextDeleteRow,
|
||||||
startEditing: contextStartEditing,
|
startEditing: contextStartEditing,
|
||||||
endEditing: contextEndEditing,
|
|
||||||
} = useRealtime();
|
} = useRealtime();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -89,17 +92,27 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
|
|
||||||
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]);
|
||||||
|
|
||||||
@ -113,6 +126,31 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
}));
|
}));
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
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 [];
|
||||||
@ -123,43 +161,59 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
columnsConfig,
|
columnsConfig,
|
||||||
onCellUpdate: handleUpdateCell,
|
onCellUpdate: handleUpdateCell,
|
||||||
onCellNumberClick: handleClickRowCell,
|
onCellNumberClick: handleClickRowCell,
|
||||||
onCellUpdateError: (rowId, columnId, error) => {
|
onCellUpdateError: handleCellUpdateError,
|
||||||
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, handleUpdateCell]);
|
}, [columnsConfig]);
|
||||||
|
|
||||||
|
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 = {
|
||||||
const tableConfig = useMemo(
|
overscan: 5,
|
||||||
() => ({
|
|
||||||
...BASE_TABLE_CONFIG,
|
|
||||||
columns,
|
|
||||||
data: data,
|
|
||||||
enableRowVirtualization: true,
|
|
||||||
enableColumnVirtualization: true,
|
|
||||||
rowVirtualizerInstanceRef: rowVirtualizerRef,
|
|
||||||
rowVirtualizerOptions: {
|
|
||||||
overscan: 30,
|
|
||||||
scrollPaddingStart: 0,
|
scrollPaddingStart: 0,
|
||||||
scrollPaddingEnd: 0,
|
scrollPaddingEnd: 0,
|
||||||
estimateSize: () => TABLE_ROW_HEIGHT,
|
estimateSize: () => TABLE_ROW_HEIGHT,
|
||||||
measureElement: (el) => el?.offsetHeight || TABLE_ROW_HEIGHT,
|
measureElement: (el) => el?.offsetHeight || TABLE_ROW_HEIGHT,
|
||||||
},
|
};
|
||||||
|
|
||||||
|
const createTableConfig = ({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
columnSizing,
|
||||||
|
columnPinning,
|
||||||
|
columnVisibility,
|
||||||
|
globalFilter,
|
||||||
|
showColumnFilters,
|
||||||
|
rowSelection,
|
||||||
|
containerRef,
|
||||||
|
tableMeta,
|
||||||
|
setColumnSizing,
|
||||||
|
setColumnPinning,
|
||||||
|
editingCell,
|
||||||
|
contextStartEditing,
|
||||||
|
}) => ({
|
||||||
|
...BASE_TABLE_CONFIG,
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
enableRowVirtualization: true,
|
||||||
|
enableColumnVirtualization: true,
|
||||||
|
rowVirtualizerInstanceRef: rowVirtualizerRef,
|
||||||
|
rowVirtualizerOptions: ROW_VIRTUALIZER_OPTIONS,
|
||||||
onEditingCellChange: (cell) => {
|
onEditingCellChange: (cell) => {
|
||||||
if (cell) {
|
if (cell && !editingCell) {
|
||||||
if (editingCell) return;
|
|
||||||
contextStartEditing?.(cell.row, cell.column);
|
contextStartEditing?.(cell.row, cell.column);
|
||||||
} else {
|
|
||||||
contextEndEditing?.(editingCell.row, editingCell.column);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
columnVirtualizerOptions: ({ table }) => ({
|
columnVirtualizerOptions: ({ table }) => ({
|
||||||
@ -175,7 +229,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
}
|
}
|
||||||
const allCols = [...table.getLeftVisibleLeafColumns(), ...table.getCenterVisibleLeafColumns()]
|
const allCols = [...table.getLeftVisibleLeafColumns(), ...table.getCenterVisibleLeafColumns()]
|
||||||
return allCols[index]?.getSize() ?? 150;
|
return allCols[index]?.getSize() ?? 150;
|
||||||
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
enableRowSelection: true,
|
enableRowSelection: true,
|
||||||
@ -199,7 +252,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
muiTableHeadCellProps: {
|
muiTableHeadCellProps: {
|
||||||
sx: { boxSizing: 'border-box' },
|
sx: { boxSizing: 'border-box' },
|
||||||
},
|
},
|
||||||
muiTableBodyCellProps: getTableHeadCellStyles,
|
muiTableBodyCellProps: getTableBodyCellProps,
|
||||||
muiTableHeadProps: {
|
muiTableHeadProps: {
|
||||||
sx: {
|
sx: {
|
||||||
display: 'table-header-group',
|
display: 'table-header-group',
|
||||||
@ -207,6 +260,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
minHeight: '1px',
|
minHeight: '1px',
|
||||||
maxHeight: '1px',
|
maxHeight: '1px',
|
||||||
visibility: 'hidden',
|
visibility: 'hidden',
|
||||||
|
'& svg': {
|
||||||
|
height: '1px',
|
||||||
|
minHeight: '1px',
|
||||||
|
maxHeight: '1px',
|
||||||
|
visibility: 'hidden',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
muiTablePaperProps: getTablePaperStyles(),
|
muiTablePaperProps: getTablePaperStyles(),
|
||||||
@ -223,64 +282,71 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
placeholder: 'Поиск...',
|
placeholder: 'Поиск...',
|
||||||
size: 'small',
|
size: 'small',
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
[
|
|
||||||
|
// Конфигурация таблицы
|
||||||
|
const tableConfig = useMemo(() => createTableConfig({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
setColumnSizing,
|
|
||||||
setColumnPinning,
|
|
||||||
columnSizing,
|
columnSizing,
|
||||||
columnPinning,
|
columnPinning,
|
||||||
columnVisibility,
|
columnVisibility,
|
||||||
globalFilter,
|
globalFilter,
|
||||||
showColumnFilters,
|
showColumnFilters,
|
||||||
|
rowSelection,
|
||||||
containerRef,
|
containerRef,
|
||||||
tableMeta,
|
tableMeta,
|
||||||
|
setColumnSizing,
|
||||||
|
setColumnPinning,
|
||||||
|
editingCell,
|
||||||
|
contextStartEditing,
|
||||||
|
}), [
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
columnSizing,
|
||||||
|
columnPinning,
|
||||||
|
columnVisibility,
|
||||||
|
globalFilter,
|
||||||
|
showColumnFilters,
|
||||||
rowSelection,
|
rowSelection,
|
||||||
editingCell
|
containerRef,
|
||||||
],
|
tableMeta,
|
||||||
);
|
setColumnSizing,
|
||||||
|
setColumnPinning,
|
||||||
|
editingCell,
|
||||||
|
]);
|
||||||
|
|
||||||
const table = useMaterialReactTable(tableConfig);
|
const table = useMaterialReactTable(tableConfig);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dataEditingCells || !dataEditingCells.line_id) {
|
if (!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) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toast.error('Ошибка редактирования ячейки');
|
|
||||||
}
|
}
|
||||||
}, [dataEditingCells, editingCell]);
|
});
|
||||||
|
}, [dataEditingCells, editingCell, table]);
|
||||||
|
|
||||||
|
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (table && data && columns.length !== 0) {
|
if (isFirstLoad && table && data && columns.length !== 0) {
|
||||||
setIsLoadingData(true)
|
setIsFirstLoad(false);
|
||||||
|
setIsLoadingData(true);
|
||||||
}
|
}
|
||||||
}, [data, columns, table])
|
}, [data, columns, table, isFirstLoad]);
|
||||||
|
|
||||||
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;
|
||||||
@ -317,10 +383,22 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
}, [rowSelection, table])
|
}, [rowSelection, table])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setData([]);
|
||||||
|
setRowSelection({});
|
||||||
|
setEditingCell(null);
|
||||||
|
if (rowVirtualizerRef.current) {
|
||||||
|
rowVirtualizerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsPanel
|
<SettingsPanel
|
||||||
selectedColumn={selectedColumn}
|
selectedColumnId={selectedColumnId}
|
||||||
|
columnPinning={columnPinning}
|
||||||
table={table}
|
table={table}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
onPinColumn={handlePinColumn}
|
onPinColumn={handlePinColumn}
|
||||||
@ -337,6 +415,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
formId={formId}
|
formId={formId}
|
||||||
sheetName={sheetName}
|
sheetName={sheetName}
|
||||||
direction={direction}
|
direction={direction}
|
||||||
|
year={year}
|
||||||
|
isProject={formType == 'PROJECT'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style={tableWrapperStyle}>
|
<div style={tableWrapperStyle}>
|
||||||
@ -352,6 +432,15 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
>
|
>
|
||||||
<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={{
|
||||||
@ -373,8 +462,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
<TableHead
|
<TableHead
|
||||||
table={table}
|
table={table}
|
||||||
style={{ zIndex: (theme) => theme.zIndex.modal - 1 }}
|
style={{ zIndex: (theme) => theme.zIndex.modal - 1 }}
|
||||||
onHeaderSelect={setSelectedColumn}
|
selectedColumnId={selectedColumnId}
|
||||||
selectHeader={selectedColumn}
|
onColumnSelect={handleColumnSelect}
|
||||||
onChangeWidth={handleSetColumnWidth}
|
onChangeWidth={handleSetColumnWidth}
|
||||||
isLoadingData={isLoadingData}
|
isLoadingData={isLoadingData}
|
||||||
/>,
|
/>,
|
||||||
@ -388,8 +477,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
onClose={() => setIsOpenModalSelectVsp(false)}
|
onClose={() => setIsOpenModalSelectVsp(false)}
|
||||||
formId={formId}
|
formId={formId}
|
||||||
onSelect={handleAddVspRow}
|
onSelect={handleAddVspRow}
|
||||||
// vspOptions={}
|
|
||||||
// selectedVSP={}
|
|
||||||
title="Выбор ВСП"
|
title="Выбор ВСП"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -18,10 +18,11 @@ 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 } from '../../../utils/exportForm';
|
import { exportSheet, exportSheetProject } from '../../../utils/exportFile';
|
||||||
|
|
||||||
const SettingPanel = ({
|
const SettingPanel = ({
|
||||||
selectedColumn,
|
selectedColumnId,
|
||||||
|
columnPinning,
|
||||||
table,
|
table,
|
||||||
columns,
|
columns,
|
||||||
onPinColumn,
|
onPinColumn,
|
||||||
@ -38,18 +39,24 @@ 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 (!selectedColumn) return;
|
if (!selectedColumnId) {
|
||||||
const pinningColumn = table.getState().columnPinning.left;
|
setIsPinned(false);
|
||||||
const isPinned = pinningColumn.includes(selectedColumn.id);
|
return;
|
||||||
setIsPinned(isPinned);
|
}
|
||||||
}, [selectedColumn, table]);
|
const pinningColumn = columnPinning?.left || table.getState().columnPinning.left || [];
|
||||||
|
const pinned = pinningColumn.includes(selectedColumnId);
|
||||||
|
setIsPinned(pinned);
|
||||||
|
}, [selectedColumnId, columnPinning, table]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedColumnIds(
|
setSelectedColumnIds(
|
||||||
@ -71,7 +78,16 @@ const SettingPanel = ({
|
|||||||
const handleClickDeleteFormula = () => { };
|
const handleClickDeleteFormula = () => { };
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
if (!formId || !sheetName || isExporting) return;
|
if (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);
|
||||||
@ -79,7 +95,7 @@ const SettingPanel = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Panel>
|
<Panel data-pin-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} /> */}
|
||||||
@ -88,19 +104,31 @@ const SettingPanel = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
data-active={isPinned}
|
data-active={isPinned}
|
||||||
onClick={() =>
|
disabled={!selectedColumnId}
|
||||||
isPinned
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
? onUnpinColumn?.(selectedColumn.id)
|
onClick={() => {
|
||||||
: onPinColumn?.(selectedColumn.id)
|
if (!selectedColumnId) {
|
||||||
|
console.warn('[ColumnPin] клик Pin: selectedColumnId пустой');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
console.log('[ColumnPin] клик Pin:', {
|
||||||
|
selectedColumnId,
|
||||||
|
isPinned,
|
||||||
|
action: isPinned ? 'unpin' : 'pin',
|
||||||
|
});
|
||||||
|
isPinned
|
||||||
|
? onUnpinColumn?.(selectedColumnId)
|
||||||
|
: onPinColumn?.(selectedColumnId);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Pin />
|
<Pin />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</GroupByObject>
|
</GroupByObject>
|
||||||
|
|
||||||
|
{!isProject &&
|
||||||
|
<>
|
||||||
<Divider orientation="vertical" flexItem />
|
<Divider orientation="vertical" flexItem />
|
||||||
|
|
||||||
<GroupByObject title="Строки">
|
<GroupByObject title="Строки">
|
||||||
<Tooltip title="Добавить строку">
|
<Tooltip title="Добавить строку">
|
||||||
<IconButton onClick={onAddRow} variant="outlined">
|
<IconButton onClick={onAddRow} variant="outlined">
|
||||||
@ -116,23 +144,8 @@ const SettingPanel = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</GroupByObject>
|
</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,9 +2,12 @@ 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 = table.getState().columnPinning.left || [];
|
const pinnedColumns = getPinnedColumnIds(table);
|
||||||
|
|
||||||
return [...allLeafColumns].sort((a, b) => {
|
return [...allLeafColumns].sort((a, b) => {
|
||||||
const aIsPinned = pinnedColumns.includes(a.id);
|
const aIsPinned = pinnedColumns.includes(a.id);
|
||||||
@ -46,7 +49,7 @@ const getPinnedColumnsWidth = (header, pinningColumn) => {
|
|||||||
|
|
||||||
const calculateLeftOffset = (table, columnOrHeader) => {
|
const calculateLeftOffset = (table, columnOrHeader) => {
|
||||||
const columnId = getColumnId(columnOrHeader);
|
const columnId = getColumnId(columnOrHeader);
|
||||||
const pinnedColumns = table.getState().columnPinning.left || [];
|
const pinnedColumns = getPinnedColumnIds(table);
|
||||||
const currentPinnedIndex = pinnedColumns.indexOf(columnId);
|
const currentPinnedIndex = pinnedColumns.indexOf(columnId);
|
||||||
|
|
||||||
let leftOffset = 0;
|
let leftOffset = 0;
|
||||||
@ -64,13 +67,13 @@ const calculateLeftOffset = (table, columnOrHeader) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isColumnPin = (table, header) => {
|
const isColumnPin = (table, header) => {
|
||||||
const pinningColumn = table.getState().columnPinning.left;
|
const pinningColumn = getPinnedColumnIds(table);
|
||||||
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 }) => {
|
const ColumnNumbersRow = ({ table, selectedColumnId, onColumnSelect }) => {
|
||||||
const sortedColumns = getSortedColumns(table);
|
const sortedColumns = getSortedColumns(table);
|
||||||
const tableState = table.getState();
|
const tableState = table.getState();
|
||||||
const filteredColumns = sortedColumns.filter(
|
const filteredColumns = sortedColumns.filter(
|
||||||
@ -95,27 +98,38 @@ const ColumnNumbersRow = ({ table }) => {
|
|||||||
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 }) => {
|
const ColumnNumberCell = ({ column, table, selectedColumnId, onColumnSelect }) => {
|
||||||
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: '#e5e5e5',
|
backgroundColor: selected ? '#589A2F' : '#FBFBFB',
|
||||||
|
color: selected ? '#ffffff' : '#5E5E5E',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -125,7 +139,9 @@ const ColumnNumberCell = ({ column, table }) => {
|
|||||||
borderBottom: 'none',
|
borderBottom: 'none',
|
||||||
position: isPinned ? 'sticky' : 'relative',
|
position: isPinned ? 'sticky' : 'relative',
|
||||||
left: isPinned ? leftOffset : 'auto',
|
left: isPinned ? leftOffset : 'auto',
|
||||||
zIndex: isPinned ? 20 : 'auto',
|
zIndex: selected ? 16 : isPinned ? 20 : 'auto',
|
||||||
|
cursor: 'pointer',
|
||||||
|
userSelect: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{originalIndex}
|
{originalIndex}
|
||||||
@ -133,9 +149,9 @@ const ColumnNumberCell = ({ column, table }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const HeaderCell = ({ header, table, onClick, hightlight, onChangeWidth }) => {
|
const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
|
||||||
const column = header.column;
|
const column = header.column;
|
||||||
const pinningColumn = table.getState().columnPinning.left || [];
|
const pinningColumn = getPinnedColumnIds(table);
|
||||||
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);
|
||||||
@ -148,7 +164,7 @@ const HeaderCell = ({ header, table, onClick, hightlight, onChangeWidth }) => {
|
|||||||
|
|
||||||
const backgroundColor = customBgColor
|
const backgroundColor = customBgColor
|
||||||
? customBgColor
|
? customBgColor
|
||||||
: (hightlight ? '#cbccce' : 'rgba(243, 244, 246, 1)');
|
: 'rgba(243, 244, 246, 1)';
|
||||||
|
|
||||||
const handleChangeWidth = (deltaWidth) => {
|
const handleChangeWidth = (deltaWidth) => {
|
||||||
const size = header.getSize();
|
const size = header.getSize();
|
||||||
@ -164,7 +180,7 @@ const HeaderCell = ({ header, table, onClick, hightlight, onChangeWidth }) => {
|
|||||||
}
|
}
|
||||||
// Для незакрепленных колонок текст должен начинаться после всех закрепленных
|
// Для незакрепленных колонок текст должен начинаться после всех закрепленных
|
||||||
// Получаем общую ширину всех закрепленных колонок
|
// Получаем общую ширину всех закрепленных колонок
|
||||||
const pinnedColumns = table.getState().columnPinning.left || [];
|
const pinnedColumns = getPinnedColumnIds(table);
|
||||||
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);
|
||||||
@ -188,6 +204,7 @@ const HeaderCell = ({ header, table, onClick, hightlight, 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',
|
||||||
@ -296,10 +313,10 @@ const FilterCell = ({ column, table }) => {
|
|||||||
onChange={(e) => handleFilterChange(e.target.value)}
|
onChange={(e) => handleFilterChange(e.target.value)}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '4px 6px',
|
padding: '0.25rem 0.375rem',
|
||||||
fontSize: '11px',
|
fontSize: '0.6875rem',
|
||||||
border: '1px solid #ddd',
|
border: '1px solid #ddd',
|
||||||
borderRadius: '4px',
|
borderRadius: '0.25rem',
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -313,23 +330,30 @@ 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: '4px' }}>
|
<div style={{ display: 'flex', gap: '0.25rem' }}> {/* 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={{ width: '50%', padding: '4px', fontSize: '11px' }}
|
style={{
|
||||||
|
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={{ width: '50%', padding: '4px', fontSize: '11px' }}
|
style={{
|
||||||
|
width: '50%',
|
||||||
|
padding: '0.25rem',
|
||||||
|
fontSize: '0.6875rem'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -344,10 +368,10 @@ const FilterCell = ({ column, table }) => {
|
|||||||
placeholder="Поиск..."
|
placeholder="Поиск..."
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '4px 6px',
|
padding: '0.25rem 0.375rem',
|
||||||
fontSize: '11px',
|
fontSize: '0.6875rem',
|
||||||
border: '1px solid #ddd',
|
border: '1px solid #ddd',
|
||||||
borderRadius: '4px',
|
borderRadius: '0.25rem',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -379,13 +403,15 @@ const FilterCell = ({ column, table }) => {
|
|||||||
|
|
||||||
export const TableHead = ({
|
export const TableHead = ({
|
||||||
table,
|
table,
|
||||||
onHeaderSelect,
|
selectedColumnId,
|
||||||
selectHeader,
|
onColumnSelect,
|
||||||
onChangeWidth,
|
onChangeWidth,
|
||||||
isLoadingData,
|
isLoadingData,
|
||||||
}) => {
|
}) => {
|
||||||
const handleHeaderClick = (header) => {
|
const handleHeaderClick = (header) => {
|
||||||
onHeaderSelect(header);
|
if (!header.subHeaders?.length && header.column?.id) {
|
||||||
|
onColumnSelect?.(header.column.id);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isLoadingData) {
|
if (!isLoadingData) {
|
||||||
@ -402,9 +428,15 @@ export const TableHead = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Строка с номерами колонок */}
|
{/* Строка с номерами колонок */}
|
||||||
<ColumnNumbersRow table={table} />
|
<ColumnNumbersRow
|
||||||
|
table={table}
|
||||||
|
selectedColumnId={selectedColumnId}
|
||||||
|
onColumnSelect={onColumnSelect}
|
||||||
|
/>
|
||||||
|
|
||||||
{table.getState().showColumnFilters && <FilterRow table={table} />}
|
{table.getState().showColumnFilters && (
|
||||||
|
<FilterRow table={table} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Существующие строки заголовков */}
|
{/* Существующие строки заголовков */}
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
@ -413,9 +445,6 @@ 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
@ -1,555 +0,0 @@
|
|||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,577 +0,0 @@
|
|||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,555 +0,0 @@
|
|||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3965
web/src/components/RealtimeTable/constants/FORM_4/AHR.js
Normal file
3965
web/src/components/RealtimeTable/constants/FORM_4/AHR.js
Normal file
File diff suppressed because it is too large
Load Diff
3963
web/src/components/RealtimeTable/constants/FORM_4/CAP.js
Normal file
3963
web/src/components/RealtimeTable/constants/FORM_4/CAP.js
Normal file
File diff suppressed because it is too large
Load Diff
3970
web/src/components/RealtimeTable/constants/FORM_4/OPER.js
Normal file
3970
web/src/components/RealtimeTable/constants/FORM_4/OPER.js
Normal file
File diff suppressed because it is too large
Load Diff
869
web/src/components/RealtimeTable/constants/FORM_4/SMETA.js
Normal file
869
web/src/components/RealtimeTable/constants/FORM_4/SMETA.js
Normal file
@ -0,0 +1,869 @@
|
|||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
3988
web/src/components/RealtimeTable/constants/FORM_4/STRUCTURE.js
Normal file
3988
web/src/components/RealtimeTable/constants/FORM_4/STRUCTURE.js
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1003
web/src/components/RealtimeTable/constants/PROJECT/LIMIT.js
Normal file
1003
web/src/components/RealtimeTable/constants/PROJECT/LIMIT.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -45,6 +45,18 @@ 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,17 +25,29 @@ 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 = `${(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}` : ""}`;
|
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")}` +
|
||||||
|
(!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;
|
||||||
}
|
}
|
||||||
@ -81,7 +93,7 @@ export const RealtimeProvider = ({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { isConnected, sendMessage, disconnect, lastError } = useWebSocket(
|
const { isConnectedRef, isConnected, sendMessage, disconnect, lastError } = useWebSocket(
|
||||||
wsUrl,
|
wsUrl,
|
||||||
handleMessage,
|
handleMessage,
|
||||||
);
|
);
|
||||||
@ -145,13 +157,26 @@ 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 (!isConnected) {
|
if (!isConnectedRef) {
|
||||||
onErrorRef.current?.("WebSocket не подключен");
|
onErrorRef.current?.("WebSocket не подключен");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -183,7 +208,7 @@ export const RealtimeProvider = ({
|
|||||||
const columnId = column.id;
|
const columnId = column.id;
|
||||||
const rowId = row.id;
|
const rowId = row.id;
|
||||||
|
|
||||||
if (!isConnected) {
|
if (!isConnectedRef) {
|
||||||
onErrorRef.current?.("WebSocket не подключен");
|
onErrorRef.current?.("WebSocket не подключен");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -213,7 +238,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 (!isConnected) {
|
if (!isConnectedRef) {
|
||||||
onErrorRef.current?.("WebSocket не подключен");
|
onErrorRef.current?.("WebSocket не подключен");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -224,6 +249,7 @@ 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,
|
||||||
},
|
},
|
||||||
@ -241,7 +267,7 @@ export const RealtimeProvider = ({
|
|||||||
|
|
||||||
const addRow = useCallback(
|
const addRow = useCallback(
|
||||||
async (rowData) => {
|
async (rowData) => {
|
||||||
if (!isConnected) {
|
if (!isConnectedRef) {
|
||||||
onErrorRef.current?.("Нет подключения или авторизации");
|
onErrorRef.current?.("Нет подключения или авторизации");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -249,10 +275,9 @@ 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],
|
||||||
@ -260,7 +285,7 @@ export const RealtimeProvider = ({
|
|||||||
|
|
||||||
const deleteRow = useCallback(
|
const deleteRow = useCallback(
|
||||||
async (rowId) => {
|
async (rowId) => {
|
||||||
if (!isConnected) {
|
if (!isConnectedRef) {
|
||||||
onErrorRef.current?.("Нет подключения или авторизации");
|
onErrorRef.current?.("Нет подключения или авторизации");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -268,10 +293,8 @@ 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,6 +41,7 @@ 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,8 +4,9 @@ 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) => {
|
const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
||||||
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);
|
||||||
@ -77,7 +78,10 @@ const useRealtimeData = (formId, sheetName, direction) => {
|
|||||||
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,30 +107,16 @@ 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 {
|
||||||
isConnected: isConnectedRef,
|
isConnectedRef,
|
||||||
|
isConnected,
|
||||||
error,
|
error,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
login,
|
|
||||||
disconnect,
|
disconnect,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState, useMemo, useCallback } 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';
|
||||||
|
|
||||||
function EditCellPortal({
|
const EditCellPortal = React.memo(({
|
||||||
cell,
|
cell,
|
||||||
table,
|
table,
|
||||||
EditCell,
|
EditCell,
|
||||||
@ -12,85 +11,71 @@ function EditCellPortal({
|
|||||||
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(false);
|
const [refTbody, setRefTbody] = useState(null);
|
||||||
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 = async (val) => {
|
const handleChange = useCallback( (val) => {
|
||||||
table.setEditingCell(null);
|
table.setEditingCell(null);
|
||||||
try {
|
try {
|
||||||
if (table.options.meta?.updateCell) {
|
if (table.options.meta?.updateCell) {
|
||||||
const success = await table.options.meta.updateCell(
|
const success = 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');
|
||||||
const errorMsg = 'Не удалось сохранить изменение';
|
onError?.('Не удалось сохранить изменение');
|
||||||
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
|
<div ref={ref} style={{ width: '100%', height: '100%' }} />
|
||||||
ref={ref}
|
{portalContent}
|
||||||
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,
|
||||||
@ -100,43 +85,105 @@ export const getTableColumns = ({
|
|||||||
onCellNumberClick,
|
onCellNumberClick,
|
||||||
}) => {
|
}) => {
|
||||||
const columnColors = { ...columnsConfig.colors };
|
const columnColors = { ...columnsConfig.colors };
|
||||||
const columns = [...columnsConfig.columns];
|
const columns = structuredClone(columnsConfig.columns);
|
||||||
|
|
||||||
const processColumns = (columns, EditCell, Cell) => {
|
const cellPropsCache = new WeakMap();
|
||||||
columns.forEach((col) => {
|
const editPropsCache = new WeakMap();
|
||||||
col.Cell = ({ cell, table, column, row }) => (
|
|
||||||
<Cell
|
const getCachedCellProps = (row, column, table) => {
|
||||||
row={row}
|
const key = `${row.id}_${column.id}`;
|
||||||
column={column}
|
|
||||||
cell={cell}
|
if (!cellPropsCache.has(row)) {
|
||||||
globalFilter={
|
cellPropsCache.set(row, {});
|
||||||
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}`]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
col.Edit = ({ cell, table, row, column }) => (
|
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) => {
|
||||||
|
col.Cell = ({ cell, table, column, row }) => {
|
||||||
|
const props = getCachedCellProps(row, column, table);
|
||||||
|
|
||||||
|
const cellProps = useMemo(() => ({
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
cell,
|
||||||
|
...props,
|
||||||
|
}), [row.id, column.id, cell.getValue(), props._hash]);
|
||||||
|
|
||||||
|
return <Cell {...cellProps} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
col.Edit = ({ cell, table, row, column }) => {
|
||||||
|
const props = getCachedEditProps(row, column, table);
|
||||||
|
|
||||||
|
const editComponent = useMemo(() => (
|
||||||
<EditCellPortal
|
<EditCellPortal
|
||||||
cell={cell}
|
cell={cell}
|
||||||
table={table}
|
table={table}
|
||||||
EditCell={EditCell}
|
EditCell={EditCell}
|
||||||
onError={onCellUpdateError}
|
onError={onCellUpdateError}
|
||||||
isEditable={row.original?.row_type === 'INPUT' || false}
|
isEditable={props.isEditable}
|
||||||
/>
|
/>
|
||||||
);
|
), [cell, table, EditCell, onCellUpdateError, props.isEditable]);
|
||||||
|
|
||||||
|
return editComponent;
|
||||||
|
};
|
||||||
|
|
||||||
// Рекурсивно обрабатываем вложенные колонки
|
|
||||||
if (col.columns?.length) {
|
if (col.columns?.length) {
|
||||||
processColumns(col.columns, EditCell, Cell);
|
processColumns(col.columns);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
processColumns(columns, EditCell, Cell);
|
processColumns(columns);
|
||||||
|
|
||||||
const rowNumber = {
|
const rowNumber = {
|
||||||
accessorKey: 'sort_order',
|
accessorKey: 'sort_order',
|
||||||
@ -144,9 +191,11 @@ export const getTableColumns = ({
|
|||||||
size: 50,
|
size: 50,
|
||||||
enableEditing: false,
|
enableEditing: false,
|
||||||
Cell: ({ cell, row }) => {
|
Cell: ({ cell, row }) => {
|
||||||
return <button onClick={() => { onCellNumberClick(row.id) }}>
|
const handleClick = useCallback(() => {
|
||||||
{cell.getValue() + ' '}
|
onCellNumberClick(row.id);
|
||||||
</button>
|
}, [row.id, onCellNumberClick]);
|
||||||
|
|
||||||
|
return <button onClick={handleClick}>{cell.getValue() + ' '}</button>;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
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) {
|
||||||
@ -44,6 +46,29 @@ 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;
|
||||||
|
|
||||||
@ -61,7 +86,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] = { ...row.data[key], ...newData[key] };
|
row.data[key] = deepMerge(row.data[key], newData[key]);
|
||||||
} else {
|
} else {
|
||||||
row.data[key] = newData[key];
|
row.data[key] = newData[key];
|
||||||
}
|
}
|
||||||
@ -90,6 +115,7 @@ 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;
|
||||||
@ -107,24 +133,30 @@ 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 (row.data.header.expense_item_id === expense_item_id) {
|
if (
|
||||||
|
(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);
|
||||||
return true;
|
adding = 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;
|
||||||
@ -133,21 +165,26 @@ 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 (row.data.line_id == lineId) {
|
if (rowId == lineId) {
|
||||||
|
rowDeleteSortOrder = row.sort_order;
|
||||||
rows.splice(i, 1);
|
rows.splice(i, 1);
|
||||||
return true;
|
i--;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
const found = findAndDelete(row.subRows);
|
findAndDelete(row.subRows);
|
||||||
if (found) return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
findAndDelete(updatedRows);
|
findAndDelete(updatedRows);
|
||||||
return updatedRows;
|
return updatedRows;
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
|
export function getDataRowId(data) {
|
||||||
|
return data?.id || data?.line_id;
|
||||||
|
}
|
||||||
|
|
||||||
export function getRowId(row) {
|
export function getRowId(row) {
|
||||||
return row.original.data.id || row.original.data.line_id;
|
return getDataRowId(row.original.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
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';
|
import { STAGE_ROLE_RUSSIAN_NAME } from '../../constants/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';
|
import { ROLES_ID_RUSSIAN_NAME, EDIT_ACCESS_RUSSIAN } from '../../constants/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