From 098efcb497b3b1b318f6eba8474bec5e4021c953 Mon Sep 17 00:00:00 2001 From: tsygankoviva Date: Mon, 8 Jun 2026 16:09:11 +0300 Subject: [PATCH] =?UTF-8?q?create-form-back:=20=D0=B2=20=D1=80=D1=83=D1=87?= =?UTF-8?q?=D0=BA=D1=83=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D1=81=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=BE=D0=BA=20=D1=81=D1=81=D0=BF,=20=D0=BF=D0=BE=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=BF=D0=B0=D0=B4=D0=B0=D1=8E=D1=89?= =?UTF-8?q?=D0=B8=D0=B9=20=D1=82=D0=B5=D1=81=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/api/v1/forms.py | 21 +++++++++------ api/src/domain/schemas.py | 2 +- api/src/repository/org_unit_repository.py | 7 ++++- api/src/services/budget_form_service.py | 21 ++++++++++++++- api/src/services/org_unit_service.py | 2 ++ api/tests/integration/test_forms_api_smoke.py | 19 +++++++------ .../unit/test_export_service_query_params.py | 27 ++++++++++++++++++- 7 files changed, 79 insertions(+), 20 deletions(-) diff --git a/api/src/api/v1/forms.py b/api/src/api/v1/forms.py index 0760cac..467a6b6 100644 --- a/api/src/api/v1/forms.py +++ b/api/src/api/v1/forms.py @@ -363,24 +363,29 @@ async def add_form( form: FormCreateSchema, db: AsyncSession = Depends(get_db), current_user: AppUser = Depends(require_admin), -) -> BaseSingleResponse[BudgetFormResponse]: +) -> BaseListResponse[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} не найден") + org_units = await org_service.get_list(user=current_user, org_unit_id=form.org_unit_ids) + org_unit_ids = [o.id for o in org_units] + if len(org_units) != len(form.org_unit_ids): + not_found_ids = ", ".join( + [str(oid) for oid in set(form.org_unit_ids) - set(org_unit_ids)], + ) + raise HTTPException(404, f"ССП {not_found_ids} не найдены") - new_form = await bf_service.create( + new_forms = await bf_service.bulk_create( form_type_code=form.form_type_code, year=form.year, - org_unit_id=org_unit.id, + org_unit_ids=org_unit_ids, ) 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), + return BaseListResponse( + result=[BudgetFormResponse.model_validate(new_form) for new_form in new_forms], + count=len(new_forms), ) diff --git a/api/src/domain/schemas.py b/api/src/domain/schemas.py index da2a7e3..b7ef899 100644 --- a/api/src/domain/schemas.py +++ b/api/src/domain/schemas.py @@ -223,7 +223,7 @@ class FormTypeSchemaEnum(str, enum.Enum): class FormCreateSchema(BaseModel): year: int form_type_code: FormTypeSchemaEnum - org_unit_id: int + org_unit_ids: list[int] = Field(..., min_length=1) class OrgUnitBaseSchema(BaseModel): diff --git a/api/src/repository/org_unit_repository.py b/api/src/repository/org_unit_repository.py index 91ee8f4..590acaf 100644 --- a/api/src/repository/org_unit_repository.py +++ b/api/src/repository/org_unit_repository.py @@ -20,6 +20,7 @@ class OrgUnitRepository: limit: int | None = None, is_ssp: bool | None = None, is_active: bool | None = None, + org_unit_id: int | list[int] | None = None, with_count: bool = False, load_users: bool = False, ) -> list[OrgUnit] | tuple[int, list[OrgUnit]]: @@ -32,7 +33,11 @@ class OrgUnitRepository: where.append(OrgUnit.is_ssp == is_ssp) if is_active is not None: where.append(OrgUnit.is_active == is_active) - + if org_unit_id is not None: + if isinstance(org_unit_id, int): + where.append(OrgUnit.id == org_unit_id) + else: + where.append(OrgUnit.id.in_(org_unit_id)) query = query.where(*where) if load_users: diff --git a/api/src/services/budget_form_service.py b/api/src/services/budget_form_service.py index ee0809c..a266033 100644 --- a/api/src/services/budget_form_service.py +++ b/api/src/services/budget_form_service.py @@ -1,4 +1,6 @@ +import asyncio + from sqlalchemy.ext.asyncio import AsyncSession from src.repository.user_repository import UserRepository @@ -76,4 +78,21 @@ class BudgetFormService: form_type_code=form_type_code, year=year, org_unit_id=org_unit_id, - ) \ No newline at end of file + ) + + async def bulk_create( + self, + form_type_code: str, + year: int, + org_unit_ids: list[int], + ) -> BudgetForm | None: + return await asyncio.gather( + *[ + self.create( + form_type_code=form_type_code, + year=year, + org_unit_id=org_unit_id, + ) + for org_unit_id in org_unit_ids + ] + ) diff --git a/api/src/services/org_unit_service.py b/api/src/services/org_unit_service.py index 9b4bf93..e25a3a6 100644 --- a/api/src/services/org_unit_service.py +++ b/api/src/services/org_unit_service.py @@ -20,6 +20,7 @@ class OrgUnitService: limit: int | None = None, is_ssp: bool | None = None, is_active: bool | None = None, + org_unit_id: int | list[int] | None = None, with_count: bool = False, load_users: bool = False, ) -> list[OrgUnit] | tuple[int, list[OrgUnit]]: @@ -33,6 +34,7 @@ class OrgUnitService: is_ssp=is_ssp, is_active=is_active, load_users=load_users, + org_unit_id=org_unit_id, ) async def get( diff --git a/api/tests/integration/test_forms_api_smoke.py b/api/tests/integration/test_forms_api_smoke.py index 9042ba2..d3aed8e 100644 --- a/api/tests/integration/test_forms_api_smoke.py +++ b/api/tests/integration/test_forms_api_smoke.py @@ -140,17 +140,20 @@ def test_forms_delete_line_not_found_error_shape(client, admin_tokens, auth_head 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}, + json={"year": 2025, "form_type_code": "FORM_1", "org_unit_ids": [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"] + assert "result" in payload + assert isinstance(payload["result"], list) + assert len(payload["result"]) == 1 + assert "year" in payload["result"][0] + assert payload["result"][0]["year"] == 2025 + + assert "id" in payload["result"][0] + form_id = payload["result"][0]["id"] response = client.get( f"/api/v1/form/{form_id}", headers=auth_headers(admin_tokens), @@ -161,7 +164,7 @@ def test_forms_add_form_success(client, admin_tokens, auth_headers): 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}, + json={"year": 2025, "form_type_code": "FORM_1", "org_unit_ids": [1, 999]}, headers=auth_headers(admin_tokens), ) assert response.status_code == 404 @@ -173,7 +176,7 @@ def test_forms_add_form_org_unit_not_found(client, admin_tokens, auth_headers): 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}, + json={"year": 2025, "form_type_code": "FORM_1", "org_unit_ids": [1]}, headers=auth_headers(isp_tokens), ) assert response.status_code == 403 diff --git a/api/tests/unit/test_export_service_query_params.py b/api/tests/unit/test_export_service_query_params.py index bf43156..6bbbf54 100644 --- a/api/tests/unit/test_export_service_query_params.py +++ b/api/tests/unit/test_export_service_query_params.py @@ -14,6 +14,7 @@ def _build_form( form_type_code: FormTypeEnum = FormTypeEnum.FORM_1, sheet_list: list[str] | None = None, section_list: list[str] | None = None, + sheet_list_with_directions: list[dict] | None = None, ) -> SimpleNamespace: return SimpleNamespace( id=1, @@ -25,6 +26,22 @@ def _build_form( code=form_type_code, sheet_list=sheet_list or ["AHR", "SMETA"], section_list=section_list or ["plan", "q1"], + sheet_list_with_directions=sheet_list_with_directions or [ + { + "sheet_name": "AHR", + "direction": "Support", + }, + { + "sheet_name": "AHR", + "direction": "Development", + + }, + { + "sheet_name": "SMETA", + "direction": None, + + }, + ], ), ) @@ -50,7 +67,15 @@ async def test_export_sheet_rejects_direction_for_unsupported_sheet(): async def test_export_sheet_rejects_sections_for_unsupported_sheet(): service = ExportService(AsyncMock()) service.budget_form_service.get = AsyncMock( - return_value=_build_form(sheet_list=["SMETA"]) + return_value=_build_form( + sheet_list=["SMETA"], + sheet_list_with_directions=[ + { + "sheet_name": "SMETA", + "direction": None, + } + ], + ) ) service.sheet_service.get = AsyncMock(return_value=[])