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 sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.services.org_unit_service import OrgUnitService
|
||||
from src.db.models.app_user import AppUser
|
||||
from src.domain.schemas import (
|
||||
BaseListResponse,
|
||||
@ -23,18 +20,6 @@ from src.db.session import get_db
|
||||
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}")
|
||||
async def get_form_phases(
|
||||
form_id: int,
|
||||
@ -44,32 +29,14 @@ async def get_form_phases(
|
||||
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||
) -> BaseListResponse[FormPhaseResponse]:
|
||||
fp_service = FormPhaseService(db)
|
||||
org_unit_service = OrgUnitService(db)
|
||||
bf_service = BudgetFormService(db)
|
||||
|
||||
phases = await fp_service.get_list(
|
||||
budget_form_id=form_id,
|
||||
user=current_user,
|
||||
sheet=sheet,
|
||||
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(
|
||||
result=result,
|
||||
result=[FormPhaseResponse.model_validate(p) for p in phases],
|
||||
count=len(phases),
|
||||
)
|
||||
|
||||
@ -85,9 +52,6 @@ async def create_form_phase(
|
||||
form = await bf_service.get(budget_form_id=form_id, user=current_user, load_form_type=True)
|
||||
if not form:
|
||||
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)
|
||||
phase = await fp_service.create(
|
||||
@ -95,16 +59,7 @@ async def create_form_phase(
|
||||
body=body,
|
||||
user=current_user,
|
||||
)
|
||||
|
||||
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)
|
||||
return BaseSingleResponse(result=FormPhaseResponse.model_validate(phase))
|
||||
|
||||
|
||||
@router.patch("/form/{form_id}/{sheet}/{phase_code}")
|
||||
@ -129,21 +84,9 @@ async def update_form_phase(
|
||||
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=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)
|
||||
return BaseSingleResponse(result=FormPhaseResponse.model_validate(phase))
|
||||
|
||||
|
||||
@router.delete("/form/{form_id}/{sheet}/{phase_code}")
|
||||
|
||||
@ -75,7 +75,6 @@ async def create_ssp(
|
||||
org_unit = await org_unit_service.create(
|
||||
title=org_data.title,
|
||||
is_ssp=org_data.is_ssp,
|
||||
utc_offset=org_data.utc_offset,
|
||||
user=current_user,
|
||||
)
|
||||
return BaseSingleResponse(
|
||||
|
||||
@ -21,7 +21,6 @@ class OrgUnit(Base):
|
||||
title: Mapped[str] = mapped_column(String)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
is_ssp: Mapped[bool] = mapped_column(Boolean)
|
||||
utc_offset: Mapped[str] = mapped_column(String(3), default="+03")
|
||||
|
||||
@property
|
||||
def users_count(self) -> int | None:
|
||||
|
||||
@ -22,8 +22,8 @@ class PhaseTemplate(Base):
|
||||
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=False))
|
||||
closes_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=True))
|
||||
|
||||
# form_type_rel = relationship("FormType", foreign_keys=[form_type])
|
||||
# role_rel = relationship("Role", foreign_keys=[role], back_populates="phase_templates")
|
||||
|
||||
@ -235,14 +235,12 @@ class OrgUnitBaseSchema(BaseModel):
|
||||
|
||||
class OrgUnitCreateSchema(OrgUnitBaseSchema):
|
||||
title: str = Field(..., min_length=1)
|
||||
utc_offset: str = Field('+03', min_length=3, pattern=r'^[+]\d\d$')
|
||||
|
||||
|
||||
class OrgUnitUpdateSchema(OrgUnitBaseSchema):
|
||||
title: Optional[str] = None
|
||||
is_ssp: 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)
|
||||
|
||||
@ -250,7 +248,6 @@ class OrgUnitUpdateSchema(OrgUnitBaseSchema):
|
||||
class OrgUnitResponseSchema(OrgUnitBaseSchema):
|
||||
id: int
|
||||
is_active: bool
|
||||
utc_offset: str
|
||||
users_count: int | None = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@ -268,7 +265,6 @@ class OrgUnitResponseSchema(OrgUnitBaseSchema):
|
||||
"is_ssp": data.is_ssp,
|
||||
"is_ssp": data.is_ssp,
|
||||
"is_active": data.is_active,
|
||||
"utc_offset": data.utc_offset
|
||||
}
|
||||
try:
|
||||
result["users_count"] = data.users_count
|
||||
|
||||
@ -55,8 +55,8 @@ class FormPhaseRepository:
|
||||
:phase_code,
|
||||
:role,
|
||||
:column_keys,
|
||||
CAST(:opens_at AS TIMESTAMP WITHOUT TIME ZONE),
|
||||
CAST(:closes_at AS TIMESTAMP WITHOUT TIME ZONE)
|
||||
:opens_at,
|
||||
:closes_at
|
||||
)).budget_form_id
|
||||
"""
|
||||
),
|
||||
@ -87,8 +87,8 @@ class FormPhaseRepository:
|
||||
:phase_code,
|
||||
:role,
|
||||
:column_keys,
|
||||
CAST(:opens_at AS TIMESTAMP WITHOUT TIME ZONE),
|
||||
CAST(:closes_at AS TIMESTAMP WITHOUT TIME ZONE)
|
||||
:opens_at,
|
||||
:closes_at
|
||||
)).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 src.db.models.org_unit import OrgUnit
|
||||
@ -84,10 +84,9 @@ class OrgUnitRepository:
|
||||
self,
|
||||
title: str,
|
||||
is_ssp: bool,
|
||||
utc_offset: str,
|
||||
) -> OrgUnit:
|
||||
"""Создание 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)
|
||||
await self.db.flush()
|
||||
# await self.db.refresh(ssp)
|
||||
@ -99,29 +98,10 @@ class OrgUnitRepository:
|
||||
if not org_unit:
|
||||
return None
|
||||
|
||||
old_offset = org_unit.utc_offset
|
||||
for field, value in kwargs.items():
|
||||
setattr(org_unit, field, value)
|
||||
|
||||
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)
|
||||
return org_unit
|
||||
|
||||
|
||||
@ -54,7 +54,6 @@ class OrgUnitService:
|
||||
self,
|
||||
title: str,
|
||||
is_ssp: bool,
|
||||
utc_offset: str,
|
||||
user: AppUser,
|
||||
) -> OrgUnit:
|
||||
"""Создание нового ССП/РФ."""
|
||||
@ -64,7 +63,6 @@ class OrgUnitService:
|
||||
return await self.org_repo.create(
|
||||
title=title,
|
||||
is_ssp=is_ssp,
|
||||
utc_offset=utc_offset,
|
||||
)
|
||||
|
||||
async def update(
|
||||
|
||||
@ -84,16 +84,16 @@ def test_form_phases_delete_forbidden(client, isp_tokens):
|
||||
|
||||
def test_form_phases_create(client, admin_tokens):
|
||||
body = {
|
||||
"budget_form_id": 4,
|
||||
"budget_form_id": 1,
|
||||
"sheet": "AHR",
|
||||
"phase_code": "new_phase",
|
||||
"role": "DFIP",
|
||||
"column_keys": ["plan.q1"],
|
||||
"opens_at": "2026-01-01T00:00:00",
|
||||
"closes_at": "2026-12-31T00:00:00",
|
||||
"opens_at": "2026-01-01T00:00:00Z",
|
||||
"closes_at": "2026-12-31T00:00:00Z",
|
||||
}
|
||||
response = client.post(
|
||||
"/api/v1/stages/form/4",
|
||||
"/api/v1/stages/form/1",
|
||||
json=body,
|
||||
headers=_auth_headers(admin_tokens),
|
||||
)
|
||||
@ -102,31 +102,22 @@ def test_form_phases_create(client, admin_tokens):
|
||||
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 "+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 isinstance(payload["result"], dict)
|
||||
assert payload["result"] == body
|
||||
|
||||
|
||||
def test_form_phases_update(client, admin_tokens):
|
||||
body = {
|
||||
"budget_form_id": 4,
|
||||
"budget_form_id": 1,
|
||||
"sheet": "AHR",
|
||||
"phase_code": "test",
|
||||
"role": "EXECUTOR_RF",
|
||||
"column_keys": ["plan.q1"],
|
||||
"opens_at": "2026-01-01T00:00:00",
|
||||
"closes_at": "2026-12-31T00:00:00",
|
||||
"opens_at": "2026-01-01T00:00:00Z",
|
||||
"closes_at": "2026-12-31T00:00:00Z",
|
||||
}
|
||||
response = client.patch(
|
||||
"/api/v1/stages/form/4/AHR/test",
|
||||
"/api/v1/stages/form/1/AHR/test",
|
||||
json=body,
|
||||
headers=_auth_headers(admin_tokens),
|
||||
)
|
||||
@ -136,15 +127,8 @@ def test_form_phases_update(client, admin_tokens):
|
||||
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 "+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 isinstance(payload["result"], dict)
|
||||
assert payload["result"] == body
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
response = client.patch(
|
||||
"/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');
|
||||
SELECT setval('v3.app_user_id_seq', 2);
|
||||
|
||||
INSERT INTO v3.org_unit (id,title,is_active,is_ssp,utc_offset) VALUES
|
||||
(1,'Test_SSP',true,true,'+03'),
|
||||
(2,'Test РФ',true,false,'+05');
|
||||
INSERT INTO v3.org_unit (id,title,is_active,is_ssp) VALUES
|
||||
(1,'Test_SSP',true,true),
|
||||
(2,'Test РФ',true,false);
|
||||
SELECT setval('v3.org_unit_id_seq', 2);
|
||||
|
||||
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);
|
||||
|
||||
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'),
|
||||
(4,'AHR','test','DFIP','{{plan.q1}}','2029-05-05 03:00:00+05','2029-06-06 03:00:00+05');
|
||||
(1,'AHR','test','DFIP','{{plan.q1}}','2026-05-05 03:00:00+03','2026-06-06 03:00:00+03');
|
||||
|
||||
INSERT INTO v3.vsp (id,branch_id,reg_number,address,format,opened_at,placement_type,staff_count,total_area,closed_at,is_active,updated_at,is_deleted,created_at,created_by,system_code,updated_by,vsp_type,notes,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),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user