project-add-year-handler: ручка добавления года #94
@ -5,12 +5,16 @@ from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
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_executor,
|
||||
)
|
||||
from src.db.models.app_user import AppUser
|
||||
from src.db.session import get_db
|
||||
from src.domain.schemas import (
|
||||
AddForm3LineBody,
|
||||
AddProjectBody,
|
||||
AddProjectYearBody,
|
||||
BaseListResponse,
|
||||
BaseSingleResponse,
|
||||
CellPatch,
|
||||
@ -326,6 +330,21 @@ async def delete_project(
|
||||
return ResponseBase(success=True, message="Проект удалён")
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/year")
|
||||
async def add_project_year(
|
||||
project_id: int,
|
||||
body: AddProjectYearBody,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: AppUser = Depends(require_executor),
|
||||
) -> dict:
|
||||
project_service = ProjectService(db)
|
||||
return await project_service.add_project_year(
|
||||
project_id=project_id,
|
||||
year=body.year,
|
||||
user=current_user,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/projects")
|
||||
async def add_project(
|
||||
body: AddProjectBody,
|
||||
|
||||
@ -399,6 +399,10 @@ class UpdProjectBody(BaseModel):
|
||||
funding_by_ko_decision: Optional[Literal["prrs_budget", "bank_reserve"]] = None
|
||||
|
||||
|
||||
class AddProjectYearBody(BaseModel):
|
||||
year: int
|
||||
|
||||
|
||||
class UpdSmetaBody(BaseModel):
|
||||
is_in_plan: Optional[bool] = None
|
||||
is_in_plan_q2: Optional[bool] = None
|
||||
|
||||
@ -109,6 +109,21 @@ class ProjectRepository:
|
||||
total = int((await self.db.execute(count_query)).scalar() or 0)
|
||||
return total, payload
|
||||
|
||||
@staticmethod
|
||||
def _serialize_sub_row(rpy: RfProjectYear, smeta: Smeta | None) -> dict:
|
||||
return {
|
||||
"id": rpy.id,
|
||||
"year": rpy.year,
|
||||
"project": f"Смета_{rpy.year}",
|
||||
"is_in_plan": smeta.is_in_plan if smeta else None,
|
||||
"is_in_plan_q2": smeta.is_in_plan_q2 if smeta else None,
|
||||
"is_in_plan_q3": smeta.is_in_plan_q3 if smeta else None,
|
||||
"is_in_plan_q4": smeta.is_in_plan_q4 if smeta else None,
|
||||
"development_block": smeta.development_block if smeta else None,
|
||||
"reserve_to_prrs_ahr": float(smeta.reserve_to_prrs_ahr) if smeta and smeta.reserve_to_prrs_ahr is not None else None,
|
||||
"reserve_to_prrs_kv": float(smeta.reserve_to_prrs_kv) if smeta and smeta.reserve_to_prrs_kv is not None else None,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _serialize_project_with_reports(project: Project, years: list[tuple[RfProjectYear, Smeta | None]]) -> dict:
|
||||
return {
|
||||
@ -132,18 +147,7 @@ class ProjectRepository:
|
||||
"funding_by_ko_decision": project.funding_by_ko_decision,
|
||||
"created_at": project.created_at.isoformat() if project.created_at else None,
|
||||
"sub_rows": [
|
||||
{
|
||||
"id": rpy.id,
|
||||
"year": rpy.year,
|
||||
"project": f"Смета_{rpy.year}",
|
||||
"is_in_plan": smeta.is_in_plan if smeta else None,
|
||||
"is_in_plan_q2": smeta.is_in_plan_q2 if smeta else None,
|
||||
"is_in_plan_q3": smeta.is_in_plan_q3 if smeta else None,
|
||||
"is_in_plan_q4": smeta.is_in_plan_q4 if smeta else None,
|
||||
"development_block": smeta.development_block if smeta else None,
|
||||
"reserve_to_prrs_ahr": float(smeta.reserve_to_prrs_ahr) if smeta and smeta.reserve_to_prrs_ahr is not None else None,
|
||||
"reserve_to_prrs_kv": float(smeta.reserve_to_prrs_kv) if smeta and smeta.reserve_to_prrs_kv is not None else None,
|
||||
}
|
||||
ProjectRepository._serialize_sub_row(rpy, smeta)
|
||||
for rpy, smeta in years
|
||||
],
|
||||
}
|
||||
@ -546,6 +550,34 @@ class ProjectRepository:
|
||||
).scalar_one_or_none()
|
||||
return self._serialize_smeta(smeta)
|
||||
|
||||
async def add_project_year(self, project_id: int, year: int) -> dict | None:
|
||||
query = text(
|
||||
"""
|
||||
SELECT limit_report_id, current_expenses_report_id
|
||||
FROM v3.add_project_year(
|
||||
CAST(:project_id AS INT),
|
||||
CAST(:year AS INT)
|
||||
)
|
||||
"""
|
||||
)
|
||||
await self.db.execute(query, {"project_id": project_id, "year": year})
|
||||
await self.db.flush()
|
||||
|
||||
row = (
|
||||
await self.db.execute(
|
||||
select(RfProjectYear, Smeta)
|
||||
.outerjoin(Smeta, Smeta.rf_project_year_id == RfProjectYear.id)
|
||||
.where(
|
||||
RfProjectYear.project_id == project_id,
|
||||
RfProjectYear.year == year,
|
||||
)
|
||||
)
|
||||
).first()
|
||||
if not row:
|
||||
return None
|
||||
rpy, smeta = row
|
||||
return self._serialize_sub_row(rpy, smeta)
|
||||
|
||||
async def add_project(
|
||||
self,
|
||||
name: str,
|
||||
|
||||
@ -62,8 +62,14 @@ class ProjectService:
|
||||
project["reports"] = await self.project_repo.get_reports(project_id=project_id)
|
||||
return project
|
||||
|
||||
async def get_instance(self, project_id: int, user: AppUser) -> Project | None:
|
||||
org_unit_ids = await self._allowed_org_unit_ids(user)
|
||||
async def get_instance(
|
||||
self,
|
||||
project_id: int,
|
||||
user: AppUser | None = None,
|
||||
org_unit_ids: list[int] | None = None,
|
||||
) -> Project | None:
|
||||
if org_unit_ids is None and user is not None:
|
||||
org_unit_ids = await self._allowed_org_unit_ids(user)
|
||||
return await self.project_repo.get_instance(project_id=project_id, org_unit_ids=org_unit_ids)
|
||||
|
||||
async def resolve_report_id(
|
||||
@ -215,6 +221,28 @@ class ProjectService:
|
||||
await self.project_repo.upd_smeta(rf_project_year_id, data)
|
||||
return await self.project_repo.get_smeta(rf_project_year_id)
|
||||
|
||||
async def add_project_year(
|
||||
self,
|
||||
project_id: int,
|
||||
year: int,
|
||||
user: AppUser,
|
||||
) -> dict:
|
||||
org_unit_ids = await self._allowed_org_unit_ids(user)
|
||||
project = await self.get_instance(
|
||||
project_id=project_id,
|
||||
org_unit_ids=org_unit_ids,
|
||||
)
|
||||
if not project:
|
||||
raise ValidationException("Проект не найден")
|
||||
existing = await self.project_repo.resolve_rf_project_year_id(
|
||||
project_id=project_id,
|
||||
year=year,
|
||||
org_unit_ids=org_unit_ids,
|
||||
)
|
||||
if existing is not None:
|
||||
raise ValidationException(f"Год {year} у проекта уже заведён")
|
||||
return await self.project_repo.add_project_year(project_id=project_id, year=year)
|
||||
|
||||
async def add_project(
|
||||
self,
|
||||
name: str,
|
||||
|
||||
@ -337,3 +337,75 @@ def test_upd_smeta_not_found(client, admin_tokens, auth_headers):
|
||||
assert response.status_code == 400
|
||||
payload = response.json()
|
||||
assert payload.get("message") == "Смета не найдена"
|
||||
|
||||
|
||||
def test_add_project_year_smoke(client, admin_tokens, auth_headers):
|
||||
project_id, _ = _create_project(client, admin_tokens, auth_headers, year=2026)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1/projects/{project_id}/year",
|
||||
json={"year": 2027},
|
||||
headers=auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["year"] == 2027
|
||||
assert payload["project"] == "Смета_2027"
|
||||
assert payload["id"] is not None
|
||||
|
||||
project = _get_project_with_reports(client, auth_headers, admin_tokens, project_id)
|
||||
assert project is not None
|
||||
sub_years = {sr["year"] for sr in project["sub_rows"]}
|
||||
assert 2026 in sub_years
|
||||
assert 2027 in sub_years
|
||||
|
||||
|
||||
def test_add_project_year_duplicate(client, admin_tokens, auth_headers):
|
||||
project_id, _ = _create_project(client, admin_tokens, auth_headers, year=2026)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1/projects/{project_id}/year",
|
||||
json={"year": 2026},
|
||||
headers=auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 400
|
||||
payload = response.json()
|
||||
assert payload.get("message") == "Год 2026 у проекта уже заведён"
|
||||
|
||||
|
||||
def test_add_project_year_project_not_found(client, admin_tokens, auth_headers):
|
||||
response = client.post(
|
||||
"/api/v1/projects/99999/year",
|
||||
json={"year": 2027},
|
||||
headers=auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 400
|
||||
payload = response.json()
|
||||
assert payload.get("message") == "Проект не найден"
|
||||
|
||||
|
||||
def test_add_project_year_by_executor(client, admin_tokens, isp_tokens, auth_headers):
|
||||
project_id, _ = _create_project(client, admin_tokens, auth_headers, branch_id=2, year=2026)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1/projects/{project_id}/year",
|
||||
json={"year": 2027},
|
||||
headers=auth_headers(isp_tokens),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["year"] == 2027
|
||||
|
||||
|
||||
def test_add_project_year_by_executor_from_other_org(client, admin_tokens, isp_tokens, auth_headers):
|
||||
"""Исполнитель, привязанный к РФ id=2, не может добавить год проекту на ССП id=1 — ACL."""
|
||||
project_id, _ = _create_project(client, admin_tokens, auth_headers, branch_id=1, year=2026)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1/projects/{project_id}/year",
|
||||
json={"year": 2027},
|
||||
headers=auth_headers(isp_tokens),
|
||||
)
|
||||
assert response.status_code == 400
|
||||
payload = response.json()
|
||||
assert payload.get("message") == "Проект не найден"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user