From ca79fdedcf5a376d370194defd569e6d7b04b993 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 12 Nov 2026 11:47:52 +0400 Subject: [PATCH] =?UTF-8?q?feat(vsp):=20=D0=B2=D1=8B=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=B7=D0=BA=D0=B0=20=D0=BF=D0=BE=20=D1=81=D1=82=D0=B0=D1=82?= =?UTF-8?q?=D1=83=D1=81=D1=83=20=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B8=D0=BE?= =?UTF-8?q?=D0=B4=D1=83,=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D1=82=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=92=D0=A1=D0=9F=20=D0=B2=20dropdown=20=D0=BF=D0=BE?= =?UTF-8?q?=20include=5Finactive=20(AURORA-1883,=20#330)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- api/src/api/v1/vsp.py | 8 ++++- api/src/domain/schemas.py | 3 ++ api/src/services/vsp_service.py | 11 +++++-- api/tests/integration/test_vsp.py | 39 ++++++++++++++++++++++++ api/tests/unit/test_vsp_status.py | 50 +++++++++++++++++++++++++++++++ 5 files changed, 108 insertions(+), 3 deletions(-) diff --git a/api/src/api/v1/vsp.py b/api/src/api/v1/vsp.py index 937bf72..44501e7 100644 --- a/api/src/api/v1/vsp.py +++ b/api/src/api/v1/vsp.py @@ -67,12 +67,15 @@ async def get_vsp( async def get_vsp_dropdown( ssp_id: int | None = None, form_id: int | None = None, + include_inactive: bool = False, db: AsyncSession = Depends(get_db), current_user: AppUser = Depends(require_executor), ): """Получение записей для выпадающего списка INFO.""" service = VSPService(db) - result = await service.get_dropdown(user=current_user, ssp_id=ssp_id, form_id=form_id, with_count=True) + result = await service.get_dropdown( + user=current_user, ssp_id=ssp_id, form_id=form_id, with_count=True, include_inactive=include_inactive, + ) return BaseListResponse( success=True, message="Список INFO для выпадающего списка", @@ -99,6 +102,9 @@ async def export_info( placement_type=body.placement_type, staff_count_min=body.staff_count_min, staff_count_max=body.staff_count_max, + status=body.status, + close_date_start=body.close_date_start, + close_date_end=body.close_date_end, ) return StreamingResponse( stream, diff --git a/api/src/domain/schemas.py b/api/src/domain/schemas.py index d5cd68e..e088448 100644 --- a/api/src/domain/schemas.py +++ b/api/src/domain/schemas.py @@ -784,6 +784,9 @@ class VSPExportRequest(BaseModel): placement_type: Optional[str] = None staff_count_min: Optional[int] = None staff_count_max: Optional[int] = None + status: VSPStatus = "active" + close_date_start: Optional[date] = None + close_date_end: Optional[date] = None class ExpenseItemBaseSchema(BaseModel): diff --git a/api/src/services/vsp_service.py b/api/src/services/vsp_service.py index 113ccbe..dfedaa8 100644 --- a/api/src/services/vsp_service.py +++ b/api/src/services/vsp_service.py @@ -115,6 +115,7 @@ class VSPService: ssp_id: int | None = None, form_id: int | None = None, with_count: bool = False, + include_inactive: bool = False, ) -> list[Vsp]: ssp_ids = await self._get_scoped_ssp_ids(user=user) @@ -136,9 +137,10 @@ class VSPService: ssp_ids = list(checking_ssps & set(ssp_ids)) await self._sync_closed_records() + # AURORA-1883: include_inactive — с закрытыми, для подписей ВСП в листах форм return await self.vsp_repo.get_list( ssp_ids=ssp_ids, - is_active=True, + is_active=None if include_inactive else True, with_count=with_count, ) @@ -177,6 +179,9 @@ class VSPService: placement_type: str | None = None, staff_count_min: int | None = None, staff_count_max: int | None = None, + status: VSPStatus = "active", + close_date_start: date | None = None, + close_date_end: date | None = None, ) -> tuple[io.BytesIO, str]: data = await self.get_list( user=user, @@ -188,7 +193,9 @@ class VSPService: placement_type=placement_type, staff_count_min=staff_count_min, staff_count_max=staff_count_max, - is_active=True, + is_active=vsp_status_to_is_active(status), + close_date_start=close_date_start, + close_date_end=close_date_end, load_org_unit=True, ) diff --git a/api/tests/integration/test_vsp.py b/api/tests/integration/test_vsp.py index d92cc44..3889924 100644 --- a/api/tests/integration/test_vsp.py +++ b/api/tests/integration/test_vsp.py @@ -457,3 +457,42 @@ def test_vsp_list_closed_executor_scope(client, admin_tokens, isp_tokens, auth_h regs = _list_reg_numbers(client, isp_tokens, auth_headers, f"?status={status}") assert own in regs assert foreign not in regs + + +def test_vsp_dropdown_include_inactive(client, admin_tokens, auth_headers): + closed = _create_named_vsp(client, admin_tokens, auth_headers, ssp_id=2, close_date="2021-03-15") + + with_closed = client.get( + "/api/v1/dict/info/dropdown?form_id=2&include_inactive=true", headers=auth_headers(admin_tokens) + ) + assert with_closed.status_code == 200 + by_reg = {item["registration_number"]: item for item in with_closed.json()["result"]} + assert closed in by_reg + assert by_reg[closed]["is_active"] is False + + default = client.get("/api/v1/dict/info/dropdown?form_id=2", headers=auth_headers(admin_tokens)) + assert closed not in [item["registration_number"] for item in default.json()["result"]] + + +def _export_values(client, tokens, auth_headers, body: dict) -> set: + import io + + from openpyxl import load_workbook + + response = client.post("/api/v1/dict/info/export", json=body, headers=auth_headers(tokens)) + assert response.status_code == 200, response.text + sheet = load_workbook(io.BytesIO(response.content)).active + return {cell for row in sheet.iter_rows(values_only=True) for cell in row} + + +def test_vsp_export_status_closed(client, admin_tokens, auth_headers): + active = _create_named_vsp(client, admin_tokens, auth_headers) + closed = _create_named_vsp(client, admin_tokens, auth_headers, close_date="2021-03-15") + + closed_values = _export_values(client, admin_tokens, auth_headers, {"ssp_ids": [1], "status": "closed"}) + assert closed in closed_values + assert active not in closed_values + + default_values = _export_values(client, admin_tokens, auth_headers, {"ssp_ids": [1]}) + assert active in default_values + assert closed not in default_values diff --git a/api/tests/unit/test_vsp_status.py b/api/tests/unit/test_vsp_status.py index 0dd6cbb..58f7ab7 100644 --- a/api/tests/unit/test_vsp_status.py +++ b/api/tests/unit/test_vsp_status.py @@ -6,3 +6,53 @@ from src.services.vsp_service import vsp_status_to_is_active @pytest.mark.parametrize("status, expected", [("active", True), ("closed", False), ("all", None)]) def test_vsp_status_to_is_active(status, expected): assert vsp_status_to_is_active(status) is expected + + +# --- выгрузка и dropdown (AURORA-1883, задача 2) --- +from datetime import date +from unittest.mock import AsyncMock, MagicMock + +from src.db.models import UserRoleEnum +from src.services.vsp_service import VSPService + + +def _admin(): + user = MagicMock() + user.id = 1 + user.role_id = UserRoleEnum.ADMIN + return user + + +def _dropdown_service() -> VSPService: + service = VSPService.__new__(VSPService) + service.vsp_repo = MagicMock() + service.vsp_repo.get_list = AsyncMock(return_value=(0, [])) + service.form_repo = MagicMock() + service.form_repo.get = AsyncMock(return_value=None) + service._get_scoped_ssp_ids = AsyncMock(return_value=None) + service._sync_closed_records = AsyncMock() + return service + + +async def test_get_dropdown_include_inactive_passes_none(): + service = _dropdown_service() + await service.get_dropdown(user=_admin(), ssp_id=2, include_inactive=True, with_count=True) + assert service.vsp_repo.get_list.await_args.kwargs["is_active"] is None + + +async def test_get_dropdown_default_active_only(): + service = _dropdown_service() + await service.get_dropdown(user=_admin(), ssp_id=2, with_count=True) + assert service.vsp_repo.get_list.await_args.kwargs["is_active"] is True + + +async def test_export_xlsx_status_closed(): + service = VSPService.__new__(VSPService) + service.get_list = AsyncMock(return_value=[]) + + await service.export_xlsx(user=_admin(), status="closed", close_date_start=date(2021, 1, 1)) + + kwargs = service.get_list.await_args.kwargs + assert kwargs["is_active"] is False + assert kwargs["close_date_start"] == date(2021, 1, 1) + assert kwargs["close_date_end"] is None