From 207356976514eb5e983904493516cd0dc030fd3f Mon Sep 17 00:00:00 2001 From: tsygankoviva Date: Mon, 8 Jun 2026 12:42:36 +0300 Subject: [PATCH] =?UTF-8?q?create-form:=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/api/v1/forms.py | 36 +++++++++++++-- api/src/domain/schemas.py | 6 +++ api/src/repository/budget_form_repository.py | 34 +++++++++++++- api/src/services/budget_form_service.py | 12 +++++ api/tests/integration/test_forms_api_smoke.py | 45 +++++++++++++++++++ 5 files changed, 128 insertions(+), 5 deletions(-) diff --git a/api/src/api/v1/forms.py b/api/src/api/v1/forms.py index 7c54fc3..0760cac 100644 --- a/api/src/api/v1/forms.py +++ b/api/src/api/v1/forms.py @@ -5,13 +5,14 @@ from fastapi import APIRouter, Depends, HTTPException, Response from sqlalchemy.ext.asyncio import AsyncSession +from src.services.org_unit_service import OrgUnitService from src.services.budget_line_service import BudgetLineService from src.db.models.form_type import FormTypeEnum from src.services.sheet_service import SheetService -from src.domain.schemas import AddLineSchema, BaseListResponse, BaseSingleResponse, BudgetFormResponse, CellPatch, CellsPatch, DirectionSchemaEnum, SheetFormTypeResponse, SheetResponse +from src.domain.schemas import AddLineSchema, BaseListResponse, BaseSingleResponse, BudgetFormResponse, CellPatch, CellsPatch, DirectionSchemaEnum, FormCreateSchema, SheetFormTypeResponse, SheetResponse from src.db.models.app_user import AppUser from src.services.budget_form_service import BudgetFormService -from src.api.v1.deps import get_current_active_user_with_set_db +from src.api.v1.deps import get_current_active_user_with_set_db, require_admin from src.db.session import get_db router = APIRouter(prefix="/form", tags=["forms"]) @@ -353,4 +354,33 @@ async def delete_line( return BaseListResponse( count=len(result), result=_rows_to_sheet_response(result), - ) \ No newline at end of file + ) + + +@router.post("/") +async def add_form( + response: Response, + form: FormCreateSchema, + db: AsyncSession = Depends(get_db), + current_user: AppUser = Depends(require_admin), +) -> BaseSingleResponse[BudgetFormResponse]: + t0 = time.perf_counter() + bf_service = BudgetFormService(db) + org_service = OrgUnitService(db) + + org_unit = await org_service.get(org_unit_id=form.org_unit_id, user=current_user) + if not org_unit: + raise HTTPException(404, f"ССП {form.org_unit_id} не найден") + + new_form = await bf_service.create( + form_type_code=form.form_type_code, + year=form.year, + org_unit_id=org_unit.id, + ) + + db_ms = (time.perf_counter() - t0) * 1000 + response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}" + + return BaseSingleResponse( + result = BudgetFormResponse.model_validate(new_form), + ) diff --git a/api/src/domain/schemas.py b/api/src/domain/schemas.py index 5cfe6a0..da2a7e3 100644 --- a/api/src/domain/schemas.py +++ b/api/src/domain/schemas.py @@ -220,6 +220,12 @@ class FormTypeSchemaEnum(str, enum.Enum): FORM_4 = "FORM_4" +class FormCreateSchema(BaseModel): + year: int + form_type_code: FormTypeSchemaEnum + org_unit_id: int + + class OrgUnitBaseSchema(BaseModel): title: str is_ssp: bool diff --git a/api/src/repository/budget_form_repository.py b/api/src/repository/budget_form_repository.py index 1335cbc..40ea5c5 100644 --- a/api/src/repository/budget_form_repository.py +++ b/api/src/repository/budget_form_repository.py @@ -1,4 +1,4 @@ -from sqlalchemy import func, select +from sqlalchemy import func, select, text from sqlalchemy.ext.asyncio import AsyncSession from src.db.models.budget_form import BudgetForm @@ -82,4 +82,34 @@ class BudgetFormRepository: joinedload(BudgetForm.form_type) ) return (await self.db.execute(query)).scalars().first() - \ No newline at end of file + + async def create( + self, + form_type_code: str, + year: int, + org_unit_id: int, + ) -> BudgetForm | None: + form_id = ( + await self.db.execute( + text( + """ + SELECT * + FROM v3.add_budget_form( + :form_type, + :year, + :org_unit_id + ) + """ + ), + { + "form_type": form_type_code, + "year": year, + "org_unit_id": org_unit_id, + } + ) + ).scalar_one_or_none() + if form_id is None: + return None + return await self.get( + budget_form_id=form_id, + ) \ No newline at end of file diff --git a/api/src/services/budget_form_service.py b/api/src/services/budget_form_service.py index c63de64..ee0809c 100644 --- a/api/src/services/budget_form_service.py +++ b/api/src/services/budget_form_service.py @@ -65,3 +65,15 @@ class BudgetFormService: load_form_type=load_form_type, load_org=load_org, ) + + async def create( + self, + form_type_code: str, + year: int, + org_unit_id: int, + ) -> BudgetForm | None: + return await self.bf_repo.create( + form_type_code=form_type_code, + year=year, + org_unit_id=org_unit_id, + ) \ No newline at end of file diff --git a/api/tests/integration/test_forms_api_smoke.py b/api/tests/integration/test_forms_api_smoke.py index f587926..9042ba2 100644 --- a/api/tests/integration/test_forms_api_smoke.py +++ b/api/tests/integration/test_forms_api_smoke.py @@ -135,3 +135,48 @@ def test_forms_delete_line_not_found_error_shape(client, admin_tokens, auth_head payload = response.json() assert isinstance(payload, dict) assert "message" in payload + + +def test_forms_add_form_success(client, admin_tokens, auth_headers): + response = client.post( + "/api/v1/form/", + json={"year": 2025, "form_type_code": "FORM_1", "org_unit_id": 1}, + headers=auth_headers(admin_tokens), + ) + assert response.status_code == 200 + payload = response.json() + assert "result" in payload + assert isinstance(payload["result"], dict) + assert payload["result"]["year"] == 2025 + + assert "id" in payload["result"] + form_id = payload["result"]["id"] + response = client.get( + f"/api/v1/form/{form_id}", + headers=auth_headers(admin_tokens), + ) + assert response.status_code == 200 + + +def test_forms_add_form_org_unit_not_found(client, admin_tokens, auth_headers): + response = client.post( + "/api/v1/form/", + json={"year": 2025, "form_type_code": "FORM_1", "org_unit_id": 999}, + headers=auth_headers(admin_tokens), + ) + assert response.status_code == 404 + payload = response.json() + assert isinstance(payload, dict) + assert "message" in payload + + +def test_forms_add_form_forbidden_not_admin(client, isp_tokens, auth_headers): + response = client.post( + "/api/v1/form/", + json={"year": 2025, "form_type_code": "FORM_1", "org_unit_id": 1}, + headers=auth_headers(isp_tokens), + ) + assert response.status_code == 403 + payload = response.json() + assert isinstance(payload, dict) + assert "message" in payload