diff --git a/api/src/api/v1/forms.py b/api/src/api/v1/forms.py index e54a233..06904fc 100644 --- a/api/src/api/v1/forms.py +++ b/api/src/api/v1/forms.py @@ -1,6 +1,6 @@ import logging import time -from typing import Optional +from typing import Literal, Optional from fastapi import APIRouter, Depends, HTTPException, Response @@ -41,6 +41,20 @@ def _parse_sections_csv(sections: Optional[str]) -> Optional[list[str]]: return parsed or None +def _parse_org_units_csv(org_units: Optional[str]) -> Optional[list[int]]: + if org_units is None: + return None + + values = [value.strip() for value in org_units.split(",") if value.strip()] + if not values: + return None + + try: + return [int(value) for value in values] + except ValueError as exc: + raise HTTPException(400, "org_units должен содержать ID через запятую") from exc + + def _validate_sheet_query_params( form_type_code: FormTypeEnum, sheet: str, @@ -63,16 +77,23 @@ def _validate_sheet_query_params( async def get_forms( offset: int = 0, limit: int = 100, + org_units: str | None = None, + form_type: Literal["FORM_1", "FORM_2", "FORM_4"] | None = None, + year: int | None = None, db: AsyncSession = Depends(get_db), current_user: AppUser = Depends(get_current_active_user_with_set_db), ) -> BaseListResponse[BudgetFormResponse]: """Все строки v3.budget_form для UI-выпадашки.""" + org_unit_ids = _parse_org_units_csv(org_units) bf_service = BudgetFormService(db) count, forms = await bf_service.get_list( user=current_user, with_count=True, offset=offset, limit=limit, + org_unit=org_unit_ids, + form_type=form_type, + year=year, load_org=True, ) return BaseListResponse( diff --git a/api/src/repository/budget_form_repository.py b/api/src/repository/budget_form_repository.py index 06fddd0..a97f3ab 100644 --- a/api/src/repository/budget_form_repository.py +++ b/api/src/repository/budget_form_repository.py @@ -18,6 +18,8 @@ class BudgetFormRepository: org_unit: int | list[int] | None = None, with_count: bool = False, load_org: bool = False, + form_type: FormTypeEnum | str | None = None, + year: int | None = None, ) -> list[BudgetForm] | tuple[int, list[BudgetForm]]: if with_count: query = select(func.count().over().label("total_count"), BudgetForm) @@ -30,13 +32,18 @@ class BudgetFormRepository: else: where.append(BudgetForm.org_unit_id.in_(org_unit)) - + if form_type is not None: + where.append(BudgetForm.form_type_code == form_type) + + if year is not None: + where.append(BudgetForm.year == year) + query = query.where(*where) if load_org: query = query.options( joinedload(BudgetForm.org_unit) ) - query = query.order_by(BudgetForm.id) + query = query.order_by(BudgetForm.id.desc()) if offset is not None: query = query.offset(offset) @@ -129,4 +136,4 @@ class BudgetFormRepository: 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 2289ecc..b2dee2b 100644 --- a/api/src/services/budget_form_service.py +++ b/api/src/services/budget_form_service.py @@ -1,5 +1,6 @@ import asyncio +from typing import Literal from sqlalchemy.ext.asyncio import AsyncSession @@ -24,6 +25,9 @@ class BudgetFormService: limit: int | None = None, with_count: bool = False, load_org: bool = False, + org_unit: list[int] | None = None, + form_type: Literal["FORM_1", "FORM_2", "FORM_4"] | None = None, + year: int | None = None, ) -> list[BudgetForm] | tuple[int, list[BudgetForm]]: if user.role_id == UserRoleEnum.ADMIN: return await self.bf_repo.get_list( @@ -31,17 +35,29 @@ class BudgetFormService: limit=limit, with_count=with_count, load_org=load_org, + org_unit=org_unit, + form_type=form_type, + year=year, ) user = await self.user_repo.get( user_id=user.id, load_orgs=True, ) + allowed_org_units = [ou.id for ou in user.org_units] + if org_unit is not None: + allowed_org_units = [ + org_unit_id + for org_unit_id in org_unit + if org_unit_id in allowed_org_units + ] return await self.bf_repo.get_list( offset=offset, limit=limit, - org_unit=[ou.id for ou in user.org_units], + org_unit=allowed_org_units, with_count=with_count, load_org=load_org, + form_type=form_type, + year=year, ) async def get( diff --git a/api/tests/integration/test_forms_api_smoke.py b/api/tests/integration/test_forms_api_smoke.py index 0e72371..4fb199a 100644 --- a/api/tests/integration/test_forms_api_smoke.py +++ b/api/tests/integration/test_forms_api_smoke.py @@ -10,6 +10,36 @@ def test_forms_list_smoke(client, admin_tokens, auth_headers): assert isinstance(payload["result"], list) +def test_forms_list_filters_and_orders_by_created_at_desc( + client, admin_tokens, auth_headers +): + response = client.get( + "/api/v1/form/", + params={"org_units": "1,2", "form_type": "FORM_2", "year": 2026}, + headers=auth_headers(admin_tokens), + ) + assert response.status_code == 200 + payload = response.json() + assert payload["count"] == 1 + assert [form["id"] for form in payload["result"]] == [4] + + response = client.get("/api/v1/form/", headers=auth_headers(admin_tokens)) + assert response.status_code == 200 + forms = response.json()["result"] + assert [form["id"] for form in forms] == sorted( + (form["id"] for form in forms), reverse=True + ) + + +def test_forms_list_rejects_invalid_org_units(client, admin_tokens, auth_headers): + response = client.get( + "/api/v1/form/", + params={"org_units": "1,invalid"}, + headers=auth_headers(admin_tokens), + ) + assert response.status_code == 400 + + def test_form_smoke(client, admin_tokens, auth_headers): response = client.get("/api/v1/form/1", headers=auth_headers(admin_tokens)) assert response.status_code == 200