Compare commits
No commits in common. "569138530ddfd7a6e8adcb0308e9779a9e7a4af4" and "857827813b9ea72021935540618349c4e900f27e" have entirely different histories.
569138530d
...
857827813b
@ -1,330 +0,0 @@
|
|||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
revision = "0005"
|
|
||||||
down_revision = "0004"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.execute('ALTER TABLE v3.phase_template ALTER COLUMN opens_at TYPE timestamp USING opens_at::timestamp')
|
|
||||||
op.execute('ALTER TABLE v3.phase_template ALTER COLUMN closes_at TYPE timestamp USING closes_at::timestamp')
|
|
||||||
op.execute("ALTER TABLE v3.org_unit ADD COLUMN utc_offset VARCHAR(3) DEFAULT '+03' NOT NULL")
|
|
||||||
# заводим фазу с нужной таймзоной
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.copy_template_to_form(p_form_id integer)
|
|
||||||
RETURNS integer
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_form_type VARCHAR;
|
|
||||||
v_inserted INTEGER;
|
|
||||||
v_utc_offset VARCHAR(3);
|
|
||||||
v_org_unit INTEGER;
|
|
||||||
BEGIN
|
|
||||||
SELECT form_type_code, org_unit_id INTO v_form_type, v_org_unit
|
|
||||||
FROM v3.budget_form
|
|
||||||
WHERE id = p_form_id;
|
|
||||||
|
|
||||||
IF NOT FOUND THEN
|
|
||||||
RAISE EXCEPTION 'budget_form id=% не существует', p_form_id;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
SELECT utc_offset INTO v_utc_offset
|
|
||||||
FROM v3.org_unit
|
|
||||||
WHERE id = v_org_unit;
|
|
||||||
|
|
||||||
INSERT INTO v3.form_phase
|
|
||||||
(budget_form_id, sheet, phase_code, role, column_keys, opens_at, closes_at)
|
|
||||||
SELECT p_form_id, pt.sheet, pt.phase_code, pt.role,
|
|
||||||
pt.column_keys, (pt.opens_at::text || v_utc_offset)::timestamptz,
|
|
||||||
(pt.closes_at::text || v_utc_offset)::timestamptz
|
|
||||||
FROM v3.phase_template pt
|
|
||||||
WHERE pt.form_type = v_form_type
|
|
||||||
ON CONFLICT (budget_form_id, sheet, phase_code) DO NOTHING;
|
|
||||||
|
|
||||||
GET DIAGNOSTICS v_inserted = ROW_COUNT;
|
|
||||||
RETURN v_inserted;
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
# Создавая фазу добавляем таймзону орг юнита
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.add_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying, p_role character varying, p_column_keys text[], p_opens_at timestamp, p_closes_at timestamp)
|
|
||||||
RETURNS v3.form_phase
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_phase v3.form_phase;
|
|
||||||
v_org_unit_id INT;
|
|
||||||
v_org_unit_title VARCHAR;
|
|
||||||
v_form_type_code VARCHAR;
|
|
||||||
v_year INT;
|
|
||||||
v_task_name TEXT;
|
|
||||||
v_phase_id TEXT;
|
|
||||||
v_utc_offset VARCHAR(3);
|
|
||||||
BEGIN
|
|
||||||
SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year
|
|
||||||
INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year
|
|
||||||
FROM v3.budget_form bf
|
|
||||||
LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
|
|
||||||
WHERE bf.id = p_budget_form_id;
|
|
||||||
|
|
||||||
SELECT utc_offset INTO v_utc_offset
|
|
||||||
FROM v3.org_unit
|
|
||||||
WHERE id = v_org_unit_id;
|
|
||||||
|
|
||||||
INSERT INTO v3.form_phase(
|
|
||||||
budget_form_id, sheet, phase_code, role, column_keys, opens_at, closes_at
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
p_budget_form_id,
|
|
||||||
p_sheet,
|
|
||||||
p_phase_code,
|
|
||||||
p_role,
|
|
||||||
p_column_keys,
|
|
||||||
CASE WHEN p_opens_at IS NOT NULL THEN (p_opens_at::text || v_utc_offset)::timestamptz ELSE NULL END,
|
|
||||||
CASE WHEN p_closes_at IS NOT NULL THEN (p_closes_at::text || v_utc_offset)::timestamptz ELSE NULL END
|
|
||||||
|
|
||||||
)
|
|
||||||
RETURNING * INTO v_phase;
|
|
||||||
|
|
||||||
|
|
||||||
v_task_name := trim(concat(
|
|
||||||
COALESCE(v_form_type_code, ''),
|
|
||||||
CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
|
|
||||||
CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
|
|
||||||
));
|
|
||||||
v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code);
|
|
||||||
|
|
||||||
PERFORM v3.log_event(
|
|
||||||
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
|
||||||
jsonb_build_object(
|
|
||||||
'entity_type', 'form_phase',
|
|
||||||
'entity_id', p_budget_form_id,
|
|
||||||
'action', 'create',
|
|
||||||
'phase_id', v_phase_id,
|
|
||||||
'phase_name', p_phase_code,
|
|
||||||
'task_id', p_budget_form_id,
|
|
||||||
'form_id', p_budget_form_id,
|
|
||||||
'budget_form_id', p_budget_form_id,
|
|
||||||
'org_unit_id', v_org_unit_id,
|
|
||||||
'org_unit_name', v_org_unit_title,
|
|
||||||
'sheet', p_sheet,
|
|
||||||
'phase_code', p_phase_code,
|
|
||||||
'role', v_phase.role,
|
|
||||||
'column_keys', v_phase.column_keys,
|
|
||||||
'opens_at', v_phase.opens_at,
|
|
||||||
'closes_at', v_phase.closes_at
|
|
||||||
),
|
|
||||||
p_budget_form_id, null, v_org_unit_id
|
|
||||||
);
|
|
||||||
|
|
||||||
RETURN v_phase;
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
# Обнволяя фазу добавляем таймзону орг юнита
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.upd_form_phase(p_budget_form_id integer, p_sheet character varying, p_phase_code character varying, p_role character varying DEFAULT NULL::character varying, p_column_keys text[] DEFAULT NULL::text[], p_opens_at timestamp without time zone DEFAULT NULL::timestamp without time zone, p_closes_at timestamp without time zone DEFAULT NULL::timestamp without time zone)
|
|
||||||
RETURNS v3.form_phase
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_old v3.form_phase;
|
|
||||||
v_new v3.form_phase;
|
|
||||||
v_is_extend BOOLEAN := FALSE;
|
|
||||||
v_has_non_extend_change BOOLEAN := FALSE;
|
|
||||||
v_has_window_change BOOLEAN := FALSE;
|
|
||||||
v_changes JSONB := '{}'::jsonb;
|
|
||||||
v_org_unit_id INT;
|
|
||||||
v_org_unit_title VARCHAR;
|
|
||||||
v_form_type_code VARCHAR;
|
|
||||||
v_year INT;
|
|
||||||
v_task_name TEXT;
|
|
||||||
v_phase_id TEXT;
|
|
||||||
v_utc_offset VARCHAR(3);
|
|
||||||
v_opens_at timestamptz;
|
|
||||||
v_closes_at timestamptz;
|
|
||||||
BEGIN
|
|
||||||
SELECT * INTO v_old
|
|
||||||
FROM v3.form_phase
|
|
||||||
WHERE budget_form_id = p_budget_form_id
|
|
||||||
AND sheet = p_sheet
|
|
||||||
AND phase_code = p_phase_code;
|
|
||||||
|
|
||||||
IF NOT FOUND THEN
|
|
||||||
RAISE EXCEPTION 'form_phase не найден (form_id=%, sheet=%, phase=%)',
|
|
||||||
p_budget_form_id, p_sheet, p_phase_code;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
SELECT bf.org_unit_id, ou.title, bf.form_type_code, bf.year, ou.utc_offset
|
|
||||||
INTO v_org_unit_id, v_org_unit_title, v_form_type_code, v_year, v_utc_offset
|
|
||||||
FROM v3.budget_form bf
|
|
||||||
LEFT JOIN v3.org_unit ou ON ou.id = bf.org_unit_id
|
|
||||||
WHERE bf.id = p_budget_form_id;
|
|
||||||
v_task_name := trim(concat(
|
|
||||||
COALESCE(v_form_type_code, ''),
|
|
||||||
CASE WHEN v_year IS NOT NULL THEN ' ' || v_year::TEXT ELSE '' END,
|
|
||||||
CASE WHEN v_org_unit_title IS NOT NULL THEN ' [' || v_org_unit_title || ']' ELSE '' END
|
|
||||||
));
|
|
||||||
|
|
||||||
IF p_opens_at is not null THEN
|
|
||||||
v_opens_at = (p_opens_at::text || v_utc_offset)::timestamptz;
|
|
||||||
END IF;
|
|
||||||
IF p_closes_at is not null THEN
|
|
||||||
v_closes_at = (p_closes_at::text || v_utc_offset)::timestamptz;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
UPDATE v3.form_phase
|
|
||||||
SET role = COALESCE(p_role, role),
|
|
||||||
column_keys = COALESCE(p_column_keys, column_keys),
|
|
||||||
opens_at = COALESCE(v_opens_at, opens_at),
|
|
||||||
closes_at = COALESCE(v_closes_at, closes_at)
|
|
||||||
WHERE budget_form_id = p_budget_form_id
|
|
||||||
AND sheet = p_sheet
|
|
||||||
AND phase_code = p_phase_code
|
|
||||||
RETURNING * INTO v_new;
|
|
||||||
|
|
||||||
|
|
||||||
v_phase_id := format('%s:%s:%s', p_budget_form_id, p_sheet, p_phase_code);
|
|
||||||
|
|
||||||
v_is_extend := v_new.closes_at > v_old.closes_at;
|
|
||||||
v_has_non_extend_change := v_old.role IS DISTINCT FROM v_new.role
|
|
||||||
OR v_old.column_keys IS DISTINCT FROM v_new.column_keys
|
|
||||||
OR v_old.opens_at IS DISTINCT FROM v_new.opens_at;
|
|
||||||
v_has_window_change := v_has_non_extend_change
|
|
||||||
OR (v_old.closes_at IS DISTINCT FROM v_new.closes_at AND NOT v_is_extend);
|
|
||||||
|
|
||||||
IF v_old.role IS DISTINCT FROM v_new.role THEN
|
|
||||||
v_changes := v_changes || jsonb_build_object(
|
|
||||||
'role', jsonb_build_object('before', v_old.role, 'after', v_new.role)
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
IF v_old.column_keys IS DISTINCT FROM v_new.column_keys THEN
|
|
||||||
v_changes := v_changes || jsonb_build_object(
|
|
||||||
'column_keys', jsonb_build_object('before', v_old.column_keys, 'after', v_new.column_keys)
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
IF v_old.opens_at IS DISTINCT FROM v_new.opens_at THEN
|
|
||||||
v_changes := v_changes || jsonb_build_object(
|
|
||||||
'opens_at', jsonb_build_object(
|
|
||||||
'before', v_old.opens_at,
|
|
||||||
'after', v_new.opens_at
|
|
||||||
)
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
IF v_old.closes_at IS DISTINCT FROM v_new.closes_at THEN
|
|
||||||
v_changes := v_changes || jsonb_build_object(
|
|
||||||
'closes_at', jsonb_build_object(
|
|
||||||
'before', v_old.closes_at,
|
|
||||||
'after', v_new.closes_at
|
|
||||||
)
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF v_is_extend THEN
|
|
||||||
PERFORM v3.log_event(
|
|
||||||
'ACCESS_EXTEND', 'ACCESS',
|
|
||||||
jsonb_build_object(
|
|
||||||
'phase_id', v_phase_id,
|
|
||||||
'phase_name', p_phase_code,
|
|
||||||
'task_id', p_budget_form_id,
|
|
||||||
'task_name', NULLIF(v_task_name, ''),
|
|
||||||
'form_id', p_budget_form_id,
|
|
||||||
'budget_form_id', p_budget_form_id,
|
|
||||||
'org_unit_id', v_org_unit_id,
|
|
||||||
'org_unit_name', v_org_unit_title,
|
|
||||||
'sheet', p_sheet,
|
|
||||||
'phase_code', p_phase_code,
|
|
||||||
'closes_at_before', v_old.closes_at,
|
|
||||||
'closes_at_after', v_new.closes_at
|
|
||||||
),
|
|
||||||
p_budget_form_id
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF v_has_window_change THEN
|
|
||||||
PERFORM v3.log_event(
|
|
||||||
'ACCESS_WINDOW_CHANGE', 'ACCESS',
|
|
||||||
jsonb_build_object(
|
|
||||||
'entity_type', 'form_phase',
|
|
||||||
'entity_id', p_budget_form_id,
|
|
||||||
'action', 'update',
|
|
||||||
'phase_id', v_phase_id,
|
|
||||||
'phase_name', p_phase_code,
|
|
||||||
'task_id', p_budget_form_id,
|
|
||||||
'task_name', NULLIF(v_task_name, ''),
|
|
||||||
'form_id', p_budget_form_id,
|
|
||||||
'budget_form_id', p_budget_form_id,
|
|
||||||
'org_unit_id', v_org_unit_id,
|
|
||||||
'org_unit_name', v_org_unit_title,
|
|
||||||
'sheet', p_sheet,
|
|
||||||
'phase_code', p_phase_code,
|
|
||||||
'changes', v_changes
|
|
||||||
),
|
|
||||||
p_budget_form_id, null, v_org_unit_id
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RETURN v_new;
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
# при обновлении таймзоны орг юнита обновляем актуальные фазы, связанные с ними
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE OR REPLACE FUNCTION v3.upd_phases_timezones(p_org_unit_id integer, p_old_tz varchar(3), p_new_tz varchar(3))
|
|
||||||
RETURNS integer
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
DECLARE
|
|
||||||
v_result integer;
|
|
||||||
BEGIN
|
|
||||||
IF p_old_tz = p_new_tz THEN
|
|
||||||
RETURN 0;
|
|
||||||
END IF;
|
|
||||||
WITH updated AS (
|
|
||||||
UPDATE v3.form_phase
|
|
||||||
SET opens_at = ((opens_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz,
|
|
||||||
closes_at = ((closes_at AT TIME zone (p_old_tz || '::00')::interval)::text || p_new_tz)::timestamptz
|
|
||||||
WHERE budget_form_id IN (SELECT id FROM v3.budget_form WHERE org_unit_id = p_org_unit_id)
|
|
||||||
AND (
|
|
||||||
opens_at >= CURRENT_TIMESTAMP
|
|
||||||
OR closes_at >= CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
RETURNING *
|
|
||||||
)
|
|
||||||
SELECT COUNT(*) INTO v_result FROM updated;
|
|
||||||
RETURN v_result;
|
|
||||||
|
|
||||||
END;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
# op.add_column(
|
|
||||||
# 'org_unit',
|
|
||||||
# sa.Column(
|
|
||||||
# 'utc_offset',
|
|
||||||
# sa.String(3),
|
|
||||||
# nullable=False,
|
|
||||||
# server_default='+03',
|
|
||||||
# )
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
pass
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
import os
|
|
||||||
import re
|
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
|
|
||||||
revision = "0006"
|
|
||||||
down_revision = "0005"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
_DOLLAR_TAG_RE = re.compile(r"\$\w+\$")
|
|
||||||
|
|
||||||
|
|
||||||
def _find_dollar_tag(line: str) -> Optional[str]:
|
|
||||||
m = _DOLLAR_TAG_RE.search(line.strip())
|
|
||||||
return m.group(0) if m else None
|
|
||||||
|
|
||||||
|
|
||||||
def _split_statements(sql: str) -> List[str]:
|
|
||||||
statements: List[str] = []
|
|
||||||
current: List[str] = []
|
|
||||||
in_dollar = False
|
|
||||||
dollar_tag: Optional[str] = None
|
|
||||||
|
|
||||||
for line in sql.split("\n"):
|
|
||||||
stripped = line.strip()
|
|
||||||
|
|
||||||
if not in_dollar:
|
|
||||||
tag = _find_dollar_tag(stripped)
|
|
||||||
if tag and tag.endswith("$") and tag.startswith("$"):
|
|
||||||
dollar_tag = tag
|
|
||||||
in_dollar = True
|
|
||||||
current.append(line)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if in_dollar and dollar_tag and stripped.startswith(dollar_tag):
|
|
||||||
after = stripped[len(dollar_tag):].strip()
|
|
||||||
if after == ";" or after == "":
|
|
||||||
in_dollar = False
|
|
||||||
dollar_tag = None
|
|
||||||
if after == ";":
|
|
||||||
current.append(line)
|
|
||||||
statements.append("\n".join(current))
|
|
||||||
current = []
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not in_dollar and stripped.rstrip().endswith(";"):
|
|
||||||
current.append(line)
|
|
||||||
statements.append("\n".join(current))
|
|
||||||
current = []
|
|
||||||
continue
|
|
||||||
|
|
||||||
current.append(line)
|
|
||||||
|
|
||||||
remaining = "\n".join(current).strip()
|
|
||||||
if remaining:
|
|
||||||
statements.append(remaining)
|
|
||||||
|
|
||||||
return statements
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
ddl_path = os.path.join(os.path.dirname(__file__), "sql", "0006_form3.sql")
|
|
||||||
with open(ddl_path) as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
statements = _split_statements(content)
|
|
||||||
for stmt in statements:
|
|
||||||
stripped = stmt.strip().rstrip(";").strip()
|
|
||||||
if not stripped:
|
|
||||||
continue
|
|
||||||
if all(l.strip().startswith("--") or not l.strip() for l in stripped.split("\n")):
|
|
||||||
continue
|
|
||||||
op.execute(stripped)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
pass
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,6 @@
|
|||||||
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.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,
|
||||||
@ -23,18 +20,6 @@ from src.db.session import get_db
|
|||||||
router = APIRouter(prefix="/stages", tags=["stages"])
|
router = APIRouter(prefix="/stages", tags=["stages"])
|
||||||
|
|
||||||
|
|
||||||
def timezone_from_offset(offset: str) -> datetime.timezone | None:
|
|
||||||
if not offset:
|
|
||||||
return None
|
|
||||||
|
|
||||||
hours = int(offset[1:])
|
|
||||||
return datetime.timezone(
|
|
||||||
datetime.timedelta(
|
|
||||||
hours=hours if offset[0] == '+' else -hours
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/form/{form_id}")
|
@router.get("/form/{form_id}")
|
||||||
async def get_form_phases(
|
async def get_form_phases(
|
||||||
form_id: int,
|
form_id: int,
|
||||||
@ -44,32 +29,14 @@ async def get_form_phases(
|
|||||||
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||||
) -> BaseListResponse[FormPhaseResponse]:
|
) -> BaseListResponse[FormPhaseResponse]:
|
||||||
fp_service = FormPhaseService(db)
|
fp_service = FormPhaseService(db)
|
||||||
org_unit_service = OrgUnitService(db)
|
|
||||||
bf_service = BudgetFormService(db)
|
|
||||||
|
|
||||||
phases = await fp_service.get_list(
|
phases = await fp_service.get_list(
|
||||||
budget_form_id=form_id,
|
budget_form_id=form_id,
|
||||||
user=current_user,
|
user=current_user,
|
||||||
sheet=sheet,
|
sheet=sheet,
|
||||||
phase_code=phase_code,
|
phase_code=phase_code,
|
||||||
)
|
)
|
||||||
if phases:
|
|
||||||
form = await bf_service.get(user=current_user, budget_form_id=form_id)
|
|
||||||
org_unit = await org_unit_service.get(user=current_user, org_unit_id=form.org_unit_id)
|
|
||||||
result = [FormPhaseResponse.model_validate(p) for p in phases]
|
|
||||||
if org_unit.utc_offset:
|
|
||||||
tz = timezone_from_offset(org_unit.utc_offset)
|
|
||||||
for element in result:
|
|
||||||
if element.opens_at:
|
|
||||||
element.opens_at = element.opens_at.astimezone(tz)
|
|
||||||
if element.closes_at:
|
|
||||||
element.closes_at = element.closes_at.astimezone(tz)
|
|
||||||
else:
|
|
||||||
result = []
|
|
||||||
for element in result:
|
|
||||||
pass
|
|
||||||
return BaseListResponse(
|
return BaseListResponse(
|
||||||
result=result,
|
result=[FormPhaseResponse.model_validate(p) for p in phases],
|
||||||
count=len(phases),
|
count=len(phases),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -86,25 +53,13 @@ async def create_form_phase(
|
|||||||
if not form:
|
if not form:
|
||||||
raise HTTPException(404, "Форма не найдена")
|
raise HTTPException(404, "Форма не найдена")
|
||||||
|
|
||||||
org_unit_service = OrgUnitService(db)
|
|
||||||
org_unit = await org_unit_service.get(user=current_user, org_unit_id=form.org_unit_id)
|
|
||||||
|
|
||||||
fp_service = FormPhaseService(db)
|
fp_service = FormPhaseService(db)
|
||||||
phase = await fp_service.create(
|
phase = await fp_service.create(
|
||||||
budget_form=form,
|
budget_form=form,
|
||||||
body=body,
|
body=body,
|
||||||
user=current_user,
|
user=current_user,
|
||||||
)
|
)
|
||||||
|
return BaseSingleResponse(result=FormPhaseResponse.model_validate(phase))
|
||||||
result = FormPhaseResponse.model_validate(phase)
|
|
||||||
|
|
||||||
if org_unit.utc_offset:
|
|
||||||
tz = timezone_from_offset(org_unit.utc_offset)
|
|
||||||
if result.opens_at:
|
|
||||||
result.opens_at = result.opens_at.astimezone(tz)
|
|
||||||
if result.closes_at:
|
|
||||||
result.closes_at = result.closes_at.astimezone(tz)
|
|
||||||
return BaseSingleResponse(result=result)
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/form/{form_id}/{sheet}/{phase_code}")
|
@router.patch("/form/{form_id}/{sheet}/{phase_code}")
|
||||||
@ -129,21 +84,9 @@ async def update_form_phase(
|
|||||||
body=body,
|
body=body,
|
||||||
user=current_user,
|
user=current_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not phase:
|
if not phase:
|
||||||
raise HTTPException(404, "Этап не найден")
|
raise HTTPException(404, "Этап не найден")
|
||||||
|
return BaseSingleResponse(result=FormPhaseResponse.model_validate(phase))
|
||||||
org_unit_service = OrgUnitService(db)
|
|
||||||
org_unit = await org_unit_service.get(user=current_user, org_unit_id=form.org_unit_id)
|
|
||||||
result = FormPhaseResponse.model_validate(phase)
|
|
||||||
|
|
||||||
if org_unit.utc_offset:
|
|
||||||
tz = timezone_from_offset(org_unit.utc_offset)
|
|
||||||
if result.opens_at:
|
|
||||||
result.opens_at = result.opens_at.astimezone(tz)
|
|
||||||
if result.closes_at:
|
|
||||||
result.closes_at = result.closes_at.astimezone(tz)
|
|
||||||
return BaseSingleResponse(result=result)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/form/{form_id}/{sheet}/{phase_code}")
|
@router.delete("/form/{form_id}/{sheet}/{phase_code}")
|
||||||
|
|||||||
@ -75,7 +75,6 @@ async def create_ssp(
|
|||||||
org_unit = await org_unit_service.create(
|
org_unit = await org_unit_service.create(
|
||||||
title=org_data.title,
|
title=org_data.title,
|
||||||
is_ssp=org_data.is_ssp,
|
is_ssp=org_data.is_ssp,
|
||||||
utc_offset=org_data.utc_offset,
|
|
||||||
user=current_user,
|
user=current_user,
|
||||||
)
|
)
|
||||||
return BaseSingleResponse(
|
return BaseSingleResponse(
|
||||||
|
|||||||
@ -21,7 +21,6 @@ class OrgUnit(Base):
|
|||||||
title: Mapped[str] = mapped_column(String)
|
title: Mapped[str] = mapped_column(String)
|
||||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
is_ssp: Mapped[bool] = mapped_column(Boolean)
|
is_ssp: Mapped[bool] = mapped_column(Boolean)
|
||||||
utc_offset: Mapped[str] = mapped_column(String(3), default="+03")
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def users_count(self) -> int | None:
|
def users_count(self) -> int | None:
|
||||||
|
|||||||
@ -22,8 +22,8 @@ class PhaseTemplate(Base):
|
|||||||
phase_code: Mapped[str] = mapped_column(String, primary_key=True)
|
phase_code: Mapped[str] = mapped_column(String, primary_key=True)
|
||||||
role: Mapped[str] = mapped_column(String, ForeignKey("v3.role.code"))
|
role: Mapped[str] = mapped_column(String, ForeignKey("v3.role.code"))
|
||||||
column_keys: Mapped[list[str]] = mapped_column(ARRAY(Text))
|
column_keys: Mapped[list[str]] = mapped_column(ARRAY(Text))
|
||||||
opens_at: Mapped[datetime] = mapped_column(DateTime(timezone=False))
|
opens_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||||
closes_at: Mapped[datetime] = mapped_column(DateTime(timezone=False))
|
closes_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||||
|
|
||||||
# form_type_rel = relationship("FormType", foreign_keys=[form_type])
|
# form_type_rel = relationship("FormType", foreign_keys=[form_type])
|
||||||
# role_rel = relationship("Role", foreign_keys=[role], back_populates="phase_templates")
|
# role_rel = relationship("Role", foreign_keys=[role], back_populates="phase_templates")
|
||||||
|
|||||||
@ -235,14 +235,12 @@ class OrgUnitBaseSchema(BaseModel):
|
|||||||
|
|
||||||
class OrgUnitCreateSchema(OrgUnitBaseSchema):
|
class OrgUnitCreateSchema(OrgUnitBaseSchema):
|
||||||
title: str = Field(..., min_length=1)
|
title: str = Field(..., min_length=1)
|
||||||
utc_offset: str = Field('+03', min_length=3, pattern=r'^[+]\d\d$')
|
|
||||||
|
|
||||||
|
|
||||||
class OrgUnitUpdateSchema(OrgUnitBaseSchema):
|
class OrgUnitUpdateSchema(OrgUnitBaseSchema):
|
||||||
title: Optional[str] = None
|
title: Optional[str] = None
|
||||||
is_ssp: Optional[bool] = None
|
is_ssp: Optional[bool] = None
|
||||||
is_active: Optional[bool] = None
|
is_active: Optional[bool] = None
|
||||||
utc_offset: Optional[str] = Field('+03', min_length=3, pattern=r'^[+]\d\d$')
|
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
@ -250,7 +248,6 @@ class OrgUnitUpdateSchema(OrgUnitBaseSchema):
|
|||||||
class OrgUnitResponseSchema(OrgUnitBaseSchema):
|
class OrgUnitResponseSchema(OrgUnitBaseSchema):
|
||||||
id: int
|
id: int
|
||||||
is_active: bool
|
is_active: bool
|
||||||
utc_offset: str
|
|
||||||
users_count: int | None = None
|
users_count: int | None = None
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
@ -268,7 +265,6 @@ class OrgUnitResponseSchema(OrgUnitBaseSchema):
|
|||||||
"is_ssp": data.is_ssp,
|
"is_ssp": data.is_ssp,
|
||||||
"is_ssp": data.is_ssp,
|
"is_ssp": data.is_ssp,
|
||||||
"is_active": data.is_active,
|
"is_active": data.is_active,
|
||||||
"utc_offset": data.utc_offset
|
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result["users_count"] = data.users_count
|
result["users_count"] = data.users_count
|
||||||
|
|||||||
@ -55,8 +55,8 @@ class FormPhaseRepository:
|
|||||||
:phase_code,
|
:phase_code,
|
||||||
:role,
|
:role,
|
||||||
:column_keys,
|
:column_keys,
|
||||||
CAST(:opens_at AS TIMESTAMP WITHOUT TIME ZONE),
|
:opens_at,
|
||||||
CAST(:closes_at AS TIMESTAMP WITHOUT TIME ZONE)
|
:closes_at
|
||||||
)).budget_form_id
|
)).budget_form_id
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
@ -87,8 +87,8 @@ class FormPhaseRepository:
|
|||||||
:phase_code,
|
:phase_code,
|
||||||
:role,
|
:role,
|
||||||
:column_keys,
|
:column_keys,
|
||||||
CAST(:opens_at AS TIMESTAMP WITHOUT TIME ZONE),
|
:opens_at,
|
||||||
CAST(:closes_at AS TIMESTAMP WITHOUT TIME ZONE)
|
:closes_at
|
||||||
)).budget_form_id
|
)).budget_form_id
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
from sqlalchemy import func, select, text, update
|
from sqlalchemy import func, select, update
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.db.models.org_unit import OrgUnit
|
from src.db.models.org_unit import OrgUnit
|
||||||
@ -84,10 +84,9 @@ class OrgUnitRepository:
|
|||||||
self,
|
self,
|
||||||
title: str,
|
title: str,
|
||||||
is_ssp: bool,
|
is_ssp: bool,
|
||||||
utc_offset: str,
|
|
||||||
) -> OrgUnit:
|
) -> OrgUnit:
|
||||||
"""Создание SSP."""
|
"""Создание SSP."""
|
||||||
ssp = OrgUnit(title=title, is_ssp=is_ssp, utc_offset=utc_offset)
|
ssp = OrgUnit(title=title, is_ssp=is_ssp)
|
||||||
self.db.add(ssp)
|
self.db.add(ssp)
|
||||||
await self.db.flush()
|
await self.db.flush()
|
||||||
# await self.db.refresh(ssp)
|
# await self.db.refresh(ssp)
|
||||||
@ -99,29 +98,10 @@ class OrgUnitRepository:
|
|||||||
if not org_unit:
|
if not org_unit:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
old_offset = org_unit.utc_offset
|
|
||||||
for field, value in kwargs.items():
|
for field, value in kwargs.items():
|
||||||
setattr(org_unit, field, value)
|
setattr(org_unit, field, value)
|
||||||
|
|
||||||
await self.db.flush()
|
await self.db.flush()
|
||||||
|
|
||||||
if org_unit.utc_offset != old_offset:
|
|
||||||
await self.db.execute(
|
|
||||||
text(
|
|
||||||
"""
|
|
||||||
SELECT v3.upd_phases_timezones(
|
|
||||||
:org_unit_id,
|
|
||||||
:old_tz,
|
|
||||||
:new_tz
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
{
|
|
||||||
"org_unit_id": org_unit.id,
|
|
||||||
"old_tz": old_offset,
|
|
||||||
"new_tz": org_unit.utc_offset,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
# await self.db.refresh(ssp)
|
# await self.db.refresh(ssp)
|
||||||
return org_unit
|
return org_unit
|
||||||
|
|
||||||
|
|||||||
@ -54,7 +54,6 @@ class OrgUnitService:
|
|||||||
self,
|
self,
|
||||||
title: str,
|
title: str,
|
||||||
is_ssp: bool,
|
is_ssp: bool,
|
||||||
utc_offset: str,
|
|
||||||
user: AppUser,
|
user: AppUser,
|
||||||
) -> OrgUnit:
|
) -> OrgUnit:
|
||||||
"""Создание нового ССП/РФ."""
|
"""Создание нового ССП/РФ."""
|
||||||
@ -64,7 +63,6 @@ class OrgUnitService:
|
|||||||
return await self.org_repo.create(
|
return await self.org_repo.create(
|
||||||
title=title,
|
title=title,
|
||||||
is_ssp=is_ssp,
|
is_ssp=is_ssp,
|
||||||
utc_offset=utc_offset,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def update(
|
async def update(
|
||||||
|
|||||||
@ -84,16 +84,16 @@ def test_form_phases_delete_forbidden(client, isp_tokens):
|
|||||||
|
|
||||||
def test_form_phases_create(client, admin_tokens):
|
def test_form_phases_create(client, admin_tokens):
|
||||||
body = {
|
body = {
|
||||||
"budget_form_id": 4,
|
"budget_form_id": 1,
|
||||||
"sheet": "AHR",
|
"sheet": "AHR",
|
||||||
"phase_code": "new_phase",
|
"phase_code": "new_phase",
|
||||||
"role": "DFIP",
|
"role": "DFIP",
|
||||||
"column_keys": ["plan.q1"],
|
"column_keys": ["plan.q1"],
|
||||||
"opens_at": "2026-01-01T00:00:00",
|
"opens_at": "2026-01-01T00:00:00Z",
|
||||||
"closes_at": "2026-12-31T00:00:00",
|
"closes_at": "2026-12-31T00:00:00Z",
|
||||||
}
|
}
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/stages/form/4",
|
"/api/v1/stages/form/1",
|
||||||
json=body,
|
json=body,
|
||||||
headers=_auth_headers(admin_tokens),
|
headers=_auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
@ -102,31 +102,22 @@ def test_form_phases_create(client, admin_tokens):
|
|||||||
assert "success" in payload
|
assert "success" in payload
|
||||||
assert payload["success"]
|
assert payload["success"]
|
||||||
assert "result" in payload
|
assert "result" in payload
|
||||||
result = payload["result"]
|
assert isinstance(payload["result"], dict)
|
||||||
assert isinstance(result, dict)
|
|
||||||
opens_at = result.pop("opens_at", None)
|
|
||||||
assert "+05" in opens_at
|
|
||||||
assert body.pop("opens_at") == opens_at.split("+")[0]
|
|
||||||
closes_at = result.pop("closes_at", None)
|
|
||||||
assert "+05" in closes_at
|
|
||||||
assert body.pop("closes_at") == closes_at.split("+")[0]
|
|
||||||
assert result == body
|
|
||||||
|
|
||||||
assert payload["result"] == body
|
assert payload["result"] == body
|
||||||
|
|
||||||
|
|
||||||
def test_form_phases_update(client, admin_tokens):
|
def test_form_phases_update(client, admin_tokens):
|
||||||
body = {
|
body = {
|
||||||
"budget_form_id": 4,
|
"budget_form_id": 1,
|
||||||
"sheet": "AHR",
|
"sheet": "AHR",
|
||||||
"phase_code": "test",
|
"phase_code": "test",
|
||||||
"role": "EXECUTOR_RF",
|
"role": "EXECUTOR_RF",
|
||||||
"column_keys": ["plan.q1"],
|
"column_keys": ["plan.q1"],
|
||||||
"opens_at": "2026-01-01T00:00:00",
|
"opens_at": "2026-01-01T00:00:00Z",
|
||||||
"closes_at": "2026-12-31T00:00:00",
|
"closes_at": "2026-12-31T00:00:00Z",
|
||||||
}
|
}
|
||||||
response = client.patch(
|
response = client.patch(
|
||||||
"/api/v1/stages/form/4/AHR/test",
|
"/api/v1/stages/form/1/AHR/test",
|
||||||
json=body,
|
json=body,
|
||||||
headers=_auth_headers(admin_tokens),
|
headers=_auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
@ -136,15 +127,8 @@ def test_form_phases_update(client, admin_tokens):
|
|||||||
assert "success" in payload
|
assert "success" in payload
|
||||||
assert payload["success"]
|
assert payload["success"]
|
||||||
assert "result" in payload
|
assert "result" in payload
|
||||||
result = payload["result"]
|
assert isinstance(payload["result"], dict)
|
||||||
assert isinstance(result, dict)
|
assert payload["result"] == body
|
||||||
opens_at = result.pop("opens_at", None)
|
|
||||||
assert "+05" in opens_at
|
|
||||||
assert body.pop("opens_at") == opens_at.split("+")[0]
|
|
||||||
closes_at = result.pop("closes_at", None)
|
|
||||||
assert "+05" in closes_at
|
|
||||||
assert body.pop("closes_at") == closes_at.split("+")[0]
|
|
||||||
assert result == body
|
|
||||||
|
|
||||||
|
|
||||||
def test_form_phases_delete(client, admin_tokens):
|
def test_form_phases_delete(client, admin_tokens):
|
||||||
|
|||||||
@ -78,47 +78,6 @@ def test_org_units_get_not_found(client, admin_tokens, auth_headers):
|
|||||||
# assert payload["result"]["id"] == 1
|
# assert payload["result"]["id"] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_org_units_update_offset_smoke(client, admin_tokens, auth_headers):
|
|
||||||
old_offset = "+05"
|
|
||||||
new_offset = "+04"
|
|
||||||
response = client.get(
|
|
||||||
"/api/v1/stages/form/4?sheet=AHR&phase_code=test", headers=auth_headers(admin_tokens)
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
assert "result" in payload
|
|
||||||
phases = payload["result"]
|
|
||||||
assert isinstance(phases, list)
|
|
||||||
assert len(phases)
|
|
||||||
phase_old = phases[0]
|
|
||||||
|
|
||||||
response = client.patch(
|
|
||||||
"/api/v1/org-unit/2",
|
|
||||||
json={"utc_offset": new_offset},
|
|
||||||
headers=auth_headers(admin_tokens),
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
assert payload["result"]["id"] == 2
|
|
||||||
assert payload["result"]["utc_offset"] == new_offset
|
|
||||||
|
|
||||||
response = client.get(
|
|
||||||
"/api/v1/stages/form/4?sheet=AHR&phase_code=test", headers=auth_headers(admin_tokens)
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
payload = response.json()
|
|
||||||
assert "result" in payload
|
|
||||||
phases = payload["result"]
|
|
||||||
assert isinstance(phases, list)
|
|
||||||
assert len(phases)
|
|
||||||
phase_new = phases[0]
|
|
||||||
|
|
||||||
assert new_offset in phase_new["opens_at"]
|
|
||||||
assert new_offset in phase_new["closes_at"]
|
|
||||||
assert phase_old["opens_at"].split(old_offset)[0] == phase_new["opens_at"].split(new_offset)[0]
|
|
||||||
assert phase_old["closes_at"].split(old_offset)[0] == phase_new["closes_at"].split(new_offset)[0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_org_units_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",
|
||||||
|
|||||||
@ -3,9 +3,9 @@ INSERT INTO v3.app_user (id,email,username,hashed_password,full_name,role_id,is_
|
|||||||
(2,'ispolnitel@rshb.ru','isp1','{isp_password}','Роль Исполнитель Первый',2,true,'2026-05-20 11:00:51.811967+03','2026-06-17 11:51:01.123054+03');
|
(2,'ispolnitel@rshb.ru','isp1','{isp_password}','Роль Исполнитель Первый',2,true,'2026-05-20 11:00:51.811967+03','2026-06-17 11:51:01.123054+03');
|
||||||
SELECT setval('v3.app_user_id_seq', 2);
|
SELECT setval('v3.app_user_id_seq', 2);
|
||||||
|
|
||||||
INSERT INTO v3.org_unit (id,title,is_active,is_ssp,utc_offset) VALUES
|
INSERT INTO v3.org_unit (id,title,is_active,is_ssp) VALUES
|
||||||
(1,'Test_SSP',true,true,'+03'),
|
(1,'Test_SSP',true,true),
|
||||||
(2,'Test РФ',true,false,'+05');
|
(2,'Test РФ',true,false);
|
||||||
SELECT setval('v3.org_unit_id_seq', 2);
|
SELECT setval('v3.org_unit_id_seq', 2);
|
||||||
|
|
||||||
INSERT INTO v3.user_org (id,user_id,org_unit_id) VALUES
|
INSERT INTO v3.user_org (id,user_id,org_unit_id) VALUES
|
||||||
@ -24,8 +24,7 @@ INSERT INTO v3.budget_line (id,budget_form_id,expense_item_id,"name",internal_or
|
|||||||
SELECT setval('v3.budget_line_id_seq', 1);
|
SELECT setval('v3.budget_line_id_seq', 1);
|
||||||
|
|
||||||
INSERT INTO v3.form_phase (budget_form_id,sheet,phase_code,"role",column_keys,opens_at,closes_at) VALUES
|
INSERT INTO v3.form_phase (budget_form_id,sheet,phase_code,"role",column_keys,opens_at,closes_at) VALUES
|
||||||
(1,'AHR','test','DFIP','{{plan.q1}}','2026-05-05 03:00:00+03','2026-06-06 03:00:00+03'),
|
(1,'AHR','test','DFIP','{{plan.q1}}','2026-05-05 03:00:00+03','2026-06-06 03:00:00+03');
|
||||||
(4,'AHR','test','DFIP','{{plan.q1}}','2029-05-05 03:00:00+05','2029-06-06 03:00:00+05');
|
|
||||||
|
|
||||||
INSERT INTO v3.vsp (id,branch_id,reg_number,address,format,opened_at,placement_type,staff_count,total_area,closed_at,is_active,updated_at,is_deleted,created_at,created_by,system_code,updated_by,vsp_type,notes,rent_contract_num,rent_end_date) VALUES
|
INSERT INTO v3.vsp (id,branch_id,reg_number,address,format,opened_at,placement_type,staff_count,total_area,closed_at,is_active,updated_at,is_deleted,created_at,created_by,system_code,updated_by,vsp_type,notes,rent_contract_num,rent_end_date) VALUES
|
||||||
(1,2,'Тестовый всп 1','Тестовая 12','укукк','2026-05-07','ывс',2026,230,'2026-06-16',false,'2026-06-18 15:46:38.611903',false,'2026-06-04 14:36:59.737727',1,'1233',91,'','','',NULL),
|
(1,2,'Тестовый всп 1','Тестовая 12','укукк','2026-05-07','ывс',2026,230,'2026-06-16',false,'2026-06-18 15:46:38.611903',false,'2026-06-04 14:36:59.737727',1,'1233',91,'','','',NULL),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user