Compare commits

..

No commits in common. "569138530ddfd7a6e8adcb0308e9779a9e7a4af4" and "857827813b9ea72021935540618349c4e900f27e" have entirely different histories.

15 changed files with 26 additions and 3180 deletions

View File

@ -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

View File

@ -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

View File

@ -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),
)
@ -86,25 +53,13 @@ async def create_form_phase(
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(
budget_form=form,
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}")

View File

@ -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(

View File

@ -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:

View File

@ -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")

View File

@ -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

View File

@ -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
"""
),

View File

@ -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

View File

@ -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(

View File

@ -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):

View File

@ -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",

View File

@ -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),