feat(vsp): выгрузка по статусу и периоду, закрытые ВСП в dropdown по include_inactive (AURORA-1883, #330)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-11-12 11:47:52 +04:00
parent 972dce8994
commit ca79fdedcf
5 changed files with 108 additions and 3 deletions

View File

@ -67,12 +67,15 @@ async def get_vsp(
async def get_vsp_dropdown( async def get_vsp_dropdown(
ssp_id: int | None = None, ssp_id: int | None = None,
form_id: int | None = None, form_id: int | None = None,
include_inactive: bool = False,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: AppUser = Depends(require_executor), current_user: AppUser = Depends(require_executor),
): ):
"""Получение записей для выпадающего списка INFO.""" """Получение записей для выпадающего списка INFO."""
service = VSPService(db) 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( return BaseListResponse(
success=True, success=True,
message="Список INFO для выпадающего списка", message="Список INFO для выпадающего списка",
@ -99,6 +102,9 @@ async def export_info(
placement_type=body.placement_type, placement_type=body.placement_type,
staff_count_min=body.staff_count_min, staff_count_min=body.staff_count_min,
staff_count_max=body.staff_count_max, 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( return StreamingResponse(
stream, stream,

View File

@ -784,6 +784,9 @@ class VSPExportRequest(BaseModel):
placement_type: Optional[str] = None placement_type: Optional[str] = None
staff_count_min: Optional[int] = None staff_count_min: Optional[int] = None
staff_count_max: 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): class ExpenseItemBaseSchema(BaseModel):

View File

@ -115,6 +115,7 @@ class VSPService:
ssp_id: int | None = None, ssp_id: int | None = None,
form_id: int | None = None, form_id: int | None = None,
with_count: bool = False, with_count: bool = False,
include_inactive: bool = False,
) -> list[Vsp]: ) -> list[Vsp]:
ssp_ids = await self._get_scoped_ssp_ids(user=user) ssp_ids = await self._get_scoped_ssp_ids(user=user)
@ -136,9 +137,10 @@ class VSPService:
ssp_ids = list(checking_ssps & set(ssp_ids)) ssp_ids = list(checking_ssps & set(ssp_ids))
await self._sync_closed_records() await self._sync_closed_records()
# AURORA-1883: include_inactive — с закрытыми, для подписей ВСП в листах форм
return await self.vsp_repo.get_list( return await self.vsp_repo.get_list(
ssp_ids=ssp_ids, ssp_ids=ssp_ids,
is_active=True, is_active=None if include_inactive else True,
with_count=with_count, with_count=with_count,
) )
@ -177,6 +179,9 @@ class VSPService:
placement_type: str | None = None, placement_type: str | None = None,
staff_count_min: int | None = None, staff_count_min: int | None = None,
staff_count_max: 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]: ) -> tuple[io.BytesIO, str]:
data = await self.get_list( data = await self.get_list(
user=user, user=user,
@ -188,7 +193,9 @@ class VSPService:
placement_type=placement_type, placement_type=placement_type,
staff_count_min=staff_count_min, staff_count_min=staff_count_min,
staff_count_max=staff_count_max, 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, load_org_unit=True,
) )

View File

@ -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}") regs = _list_reg_numbers(client, isp_tokens, auth_headers, f"?status={status}")
assert own in regs assert own in regs
assert foreign not 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

View File

@ -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)]) @pytest.mark.parametrize("status, expected", [("active", True), ("closed", False), ("all", None)])
def test_vsp_status_to_is_active(status, expected): def test_vsp_status_to_is_active(status, expected):
assert vsp_status_to_is_active(status) is 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