Compare commits

..

13 Commits

Author SHA1 Message Date
bac2a7a780 logs: логируем время 2026-06-26 10:00:27 +03:00
19dbc49dc0 Merge pull request 'sql-funcs-fix: поправил функции, агрегирующие данные не в том дирекшне' (#1) from sql-funcs-fix into test
Reviewed-on: #1
2026-06-26 09:45:30 +03:00
873f0d84b3 Merge pull request 'export-optimization: не красим обычные ячейки' (#4) from export-optimization into test
Reviewed-on: #4
2026-06-26 09:45:19 +03:00
59f9170e9f export-optimization: не красим обычные ячейки 2026-06-25 10:29:02 +03:00
230090ae22 Merge pull request 'vsp-dropdown-form: добавил фильтрацию по form_id в dropdown всп' (#73) from vsp-dropdown-form into test
Reviewed-on: #73
Reviewed-by: Raykov-MS <RaykovMS@avt.rshb.ru>
2026-06-24 14:56:32 +03:00
840d5f5f13 sql-funcs-fix: поправил функции, агрегирующие данные не в том дирекшне 2026-06-24 14:41:56 +03:00
ab21b0e496 vsp-dropdown-form: добавил фильтрацию по form_id в dropdown всп 2026-06-24 13:22:17 +03:00
PotapovaA
b27ee53a9e Merge pull request 'скрыть отчет 9' (#72) from fix-front into test
Reviewed-on: #72
2026-06-24 12:27:47 +03:00
PotapovaA
cc766238f2 скрыть отчет 9 2026-06-24 12:27:29 +03:00
63ae001a23 Merge pull request 'migrations: закомментил все что связано с раисовской авторизацией при debug=false' (#70) from migrations into test
Reviewed-on: #70
Reviewed-by: Raykov-MS <RaykovMS@avt.rshb.ru>
2026-06-24 11:42:22 +03:00
PotapovaA
04ee7b7fb2 Merge pull request 'fix этапов' (#71) from fix-front into test
Reviewed-on: #71
2026-06-24 11:08:58 +03:00
PotapovaA
c6502e59a7 fix этапов 2026-06-24 11:08:40 +03:00
c5c7963b5a migrations: закомментил все что связано с раисовской авторизацией при debug=false 2026-06-24 10:50:15 +03:00
19 changed files with 248 additions and 202 deletions

View File

@ -3244,7 +3244,7 @@ BEGIN
LEFT JOIN collegial_approval ca ON ca.line_id = bl.id AND s_ca LEFT JOIN collegial_approval ca ON ca.line_id = bl.id AND s_ca
LEFT JOIN ckk ck ON ck.line_id = bl.id AND (s_ckk OR s_need_bk) LEFT JOIN ckk ck ON ck.line_id = bl.id AND (s_ckk OR s_need_bk)
LEFT JOIN contract_detail cd ON cd.line_id = bl.id AND (s_cd OR s_need_bk) LEFT JOIN contract_detail cd ON cd.line_id = bl.id AND (s_cd OR s_need_bk)
WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND (p_direction is null OR bl.direction is null or bl.direction = p_direction)
AND (s_need_ap OR s_cs OR s_ca OR s_ckk OR s_cd OR s_need_bk) AND (s_need_ap OR s_cs OR s_ca OR s_ckk OR s_cd OR s_need_bk)
GROUP BY bl.expense_item_id GROUP BY bl.expense_item_id
), ),
@ -3257,7 +3257,7 @@ BEGIN
SUM(blq.transfer_to_q2) AS tq2, SUM(blq.transfer_to_q3) AS tq3, SUM(blq.transfer_to_q4) AS tq4 SUM(blq.transfer_to_q2) AS tq2, SUM(blq.transfer_to_q3) AS tq3, SUM(blq.transfer_to_q4) AS tq4
FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id
LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 1 LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 1
WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_q1 WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_q1 AND (p_direction is null OR bl.direction is null or bl.direction = p_direction)
GROUP BY bl.expense_item_id GROUP BY bl.expense_item_id
), ),
aq2 AS ( aq2 AS (
@ -3270,7 +3270,7 @@ BEGIN
SUM(blq.transfer_to_q3) AS tq3, SUM(blq.transfer_to_q4) AS tq4 SUM(blq.transfer_to_q3) AS tq3, SUM(blq.transfer_to_q4) AS tq4
FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id
LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 2 LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 2
WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_q2 WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_q2 AND (p_direction is null OR bl.direction is null or bl.direction = p_direction)
GROUP BY bl.expense_item_id GROUP BY bl.expense_item_id
), ),
aq3 AS ( aq3 AS (
@ -3283,7 +3283,7 @@ BEGIN
SUM(blq.transfer_to_q4) AS tq4 SUM(blq.transfer_to_q4) AS tq4
FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id
LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 3 LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 3
WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_q3 WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_q3 AND (p_direction is null OR bl.direction is null or bl.direction = p_direction)
GROUP BY bl.expense_item_id GROUP BY bl.expense_item_id
), ),
aq4 AS ( aq4 AS (
@ -3296,7 +3296,7 @@ BEGIN
SUM(blq.actual_spod) AS spod SUM(blq.actual_spod) AS spod
FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id
LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 4 LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id AND blq.quarter = 4
WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND (s_q4 OR s_tot) WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND (s_q4 OR s_tot) AND (p_direction is null OR bl.direction is null or bl.direction = p_direction)
GROUP BY bl.expense_item_id GROUP BY bl.expense_item_id
), ),
atot AS ( atot AS (
@ -3308,7 +3308,7 @@ BEGIN
SUM(blq.payment_amount) AS pay SUM(blq.payment_amount) AS pay
FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id FROM budget_line bl JOIN expense_item ei ON ei.id = bl.expense_item_id
LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id LEFT JOIN budget_line_quarter blq ON blq.line_id = bl.id
WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_tot WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet AND s_tot AND (p_direction is null OR bl.direction is null or bl.direction = p_direction)
GROUP BY bl.expense_item_id GROUP BY bl.expense_item_id
), ),
-- ═══ Pre-computed per-node aggregates (one pass over tw × agg) ═════════ -- ═══ Pre-computed per-node aggregates (one pass over tw × agg) ═════════
@ -4427,7 +4427,7 @@ BEGIN
LEFT JOIN v3.sequestration sg ON sg.line_id = bl.id AND sg.actor='SSP_GO' AND s_need_ap LEFT JOIN v3.sequestration sg ON sg.line_id = bl.id AND sg.actor='SSP_GO' AND s_need_ap
LEFT JOIN v3.reserve r ON r.line_id = bl.id AND s_need_ap LEFT JOIN v3.reserve r ON r.line_id = bl.id AND s_need_ap
LEFT JOIN v3.ckk ck ON ck.line_id = bl.id AND s_book LEFT JOIN v3.ckk ck ON ck.line_id = bl.id AND s_book
WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet and (p_direction is null OR bl.direction is null or bl.direction = p_direction)
AND (s_need_ap OR s_book) AND (s_need_ap OR s_book)
GROUP BY bl.expense_item_id GROUP BY bl.expense_item_id
), ),
@ -4453,7 +4453,7 @@ BEGIN
FROM v3.budget_line bl FROM v3.budget_line bl
JOIN v3.expense_item ei ON ei.id = bl.expense_item_id JOIN v3.expense_item ei ON ei.id = bl.expense_item_id
JOIN v3.budget_line_quarter q ON q.line_id = bl.id JOIN v3.budget_line_quarter q ON q.line_id = bl.id
WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet WHERE bl.budget_form_id = p_form_id AND ei.sheet = p_sheet and (p_direction is null OR bl.direction is null or bl.direction = p_direction)
GROUP BY bl.expense_item_id, q.quarter GROUP BY bl.expense_item_id, q.quarter
), ),
-- Tree-rollup -- Tree-rollup

View File

@ -6,46 +6,46 @@ from src.db.session import get_db
from src.domain.schemas import LoginRequest, RefreshRequest, Token from src.domain.schemas import LoginRequest, RefreshRequest, Token
from src.services.auth_service import AuthService from src.services.auth_service import AuthService
if not settings.DEBUG: # if not settings.DEBUG:
from raisa_fastapi_protected_api import UserInfo, get_user_dependency # from raisa_fastapi_protected_api import UserInfo, get_user_dependency
router = APIRouter(prefix="/auth", tags=["auth"]) router = APIRouter(prefix="/auth", tags=["auth"])
if settings.DEBUG: # if settings.DEBUG:
@router.post("/login", response_model=Token) @router.post("/login", response_model=Token)
async def login( async def login(
login_data: LoginRequest, login_data: LoginRequest,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
auth_service = AuthService(db) auth_service = AuthService(db)
token = await auth_service.authenticate_user(login_data.username, login_data.password) token = await auth_service.authenticate_user(login_data.username, login_data.password)
if not token: if not token:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Неверное имя пользователя или пароль", detail="Неверное имя пользователя или пароль",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
return token return token
else: # else:
@router.post("/login", response_model=Token) # @router.post("/login", response_model=Token)
async def login( # async def login(
login_data: LoginRequest, # login_data: LoginRequest,
db: AsyncSession = Depends(get_db), # db: AsyncSession = Depends(get_db),
user: UserInfo = Depends(get_user_dependency), # user: UserInfo = Depends(get_user_dependency),
): # ):
auth_service = AuthService(db) # auth_service = AuthService(db)
token = await auth_service.authenticate_user_via_email(user.email) # token = await auth_service.authenticate_user_via_email(user.email)
if not token: # if not token:
raise HTTPException( # raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, # status_code=status.HTTP_401_UNAUTHORIZED,
detail="Неверное имя пользователя или пароль", # detail="Неверное имя пользователя или пароль",
headers={"WWW-Authenticate": "Bearer"}, # headers={"WWW-Authenticate": "Bearer"},
) # )
return token # return token
@router.post("/login-form", response_model=Token) @router.post("/login-form", response_model=Token)

View File

@ -12,62 +12,62 @@ from src.repository.user_repository import UserRepository
security = HTTPBearer() security = HTTPBearer()
if settings.DEBUG: # if settings.DEBUG:
async def get_user_by_token( async def get_user_by_token(
token: str, token: str,
db: AsyncSession, db: AsyncSession,
) -> AppUser: ) -> AppUser:
payload = verify_token(token) payload = verify_token(token)
if payload is None: if payload is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Недействительный токен", detail="Недействительный токен",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
username: str | None = payload.get("sub") username: str | None = payload.get("sub")
if username is None: if username is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Недействительный токен", detail="Недействительный токен",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
user_repo = UserRepository(db) user_repo = UserRepository(db)
user = await user_repo.get_by_username(username) user = await user_repo.get_by_username(username)
if user is None: if user is None:
user = await user_repo.get_by_email(username) user = await user_repo.get_by_email(username)
if user is None: if user is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Пользователь не найден", detail="Пользователь не найден",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
return user return user
async def get_current_user( async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security), credentials: HTTPAuthorizationCredentials = Depends(security),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
) -> AppUser: ) -> AppUser:
return await get_user_by_token(token=credentials.credentials, db=db) return await get_user_by_token(token=credentials.credentials, db=db)
else: # else:
from raisa_fastapi_protected_api import UserInfo, get_user_dependency # from raisa_fastapi_protected_api import UserInfo, get_user_dependency
async def get_current_user( # async def get_current_user(
user: UserInfo = Depends(get_user_dependency), # user: UserInfo = Depends(get_user_dependency),
db: AsyncSession = Depends(get_db), # db: AsyncSession = Depends(get_db),
) -> AppUser: # ) -> AppUser:
user_repo = UserRepository(db) # user_repo = UserRepository(db)
db_user = await user_repo.get_by_email(user.email) # db_user = await user_repo.get_by_email(user.email)
if db_user is None: # if db_user is None:
raise HTTPException( # raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, # status_code=status.HTTP_401_UNAUTHORIZED,
detail="Пользователь не найден", # detail="Пользователь не найден",
headers={"WWW-Authenticate": "Bearer"}, # headers={"WWW-Authenticate": "Bearer"},
) # )
return db_user # return db_user
async def get_current_active_user(current_user: AppUser = Depends(get_current_user)) -> AppUser: async def get_current_active_user(current_user: AppUser = Depends(get_current_user)) -> AppUser:

View File

@ -59,12 +59,13 @@ async def get_vsp(
@router.get("/dropdown", response_model=BaseListResponse[VSPInDB]) @router.get("/dropdown", response_model=BaseListResponse[VSPInDB])
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,
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, with_count=True) result = await service.get_dropdown(user=current_user, ssp_id=ssp_id, form_id=form_id, with_count=True)
return BaseListResponse( return BaseListResponse(
success=True, success=True,
message="Список INFO для выпадающего списка", message="Список INFO для выпадающего списка",

View File

@ -22,18 +22,18 @@ from src.db.base import engine
from src.db.session import create_tables from src.db.session import create_tables
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
# if not settings.DEBUG:
if not settings.DEBUG: # from raisa_fastapi_protected_api import (
from raisa_fastapi_protected_api import ( # AuthorizationMiddleware,
AuthorizationMiddleware, # OpenEndpoint,
OpenEndpoint, # ProtectedOAuthSettings,
ProtectedOAuthSettings, # ProtectedRolesSettings,
ProtectedRolesSettings, # ProtectedSettings,
ProtectedSettings, # SearchType,
SearchType, # )
)
_AUDIT_CLEANUP_LOCK_KEY = 21987431 _AUDIT_CLEANUP_LOCK_KEY = 21987431
@ -104,13 +104,13 @@ async def lifespan(app: FastAPI):
cleanup_task: asyncio.Task | None = None cleanup_task: asyncio.Task | None = None
if not settings.DEBUG: if not settings.DEBUG:
await create_tables() await create_tables()
ProtectedSettings( # ProtectedSettings(
ProtectedOAuthSettings(JWKS_URI=settings.JWKS_URL), # ProtectedOAuthSettings(JWKS_URI=settings.JWKS_URL),
ProtectedRolesSettings( # ProtectedRolesSettings(
APP_NAMESPACE=settings.APP_NAMESPACE, # APP_NAMESPACE=settings.APP_NAMESPACE,
APP_NAME=settings.APP_NAME, # APP_NAME=settings.APP_NAME,
), # ),
) # )
cleanup_task = asyncio.create_task(_audit_cleanup_loop()) cleanup_task = asyncio.create_task(_audit_cleanup_loop())
try: try:
yield yield
@ -185,41 +185,41 @@ async def readyz(response: Response):
if not settings.DEBUG: # if not settings.DEBUG:
app.add_middleware( # app.add_middleware(
AuthorizationMiddleware, # AuthorizationMiddleware,
open_endpoints=[ # open_endpoints=[
OpenEndpoint(path="", type_search=SearchType.ABSOLUTE), # OpenEndpoint(path="", type_search=SearchType.ABSOLUTE),
OpenEndpoint(path=settings.ROOT_PATH + "", type_search=SearchType.ABSOLUTE), # OpenEndpoint(path=settings.ROOT_PATH + "", type_search=SearchType.ABSOLUTE),
OpenEndpoint(path="/", type_search=SearchType.ABSOLUTE), # OpenEndpoint(path="/", type_search=SearchType.ABSOLUTE),
OpenEndpoint(path=settings.ROOT_PATH + "/", type_search=SearchType.ABSOLUTE), # OpenEndpoint(path=settings.ROOT_PATH + "/", type_search=SearchType.ABSOLUTE),
OpenEndpoint(path="/healthcheck", type_search=SearchType.ABSOLUTE), # OpenEndpoint(path="/healthcheck", type_search=SearchType.ABSOLUTE),
OpenEndpoint( # OpenEndpoint(
path=settings.ROOT_PATH + "/healthcheck", # path=settings.ROOT_PATH + "/healthcheck",
type_search=SearchType.ABSOLUTE, # type_search=SearchType.ABSOLUTE,
), # ),
OpenEndpoint(path="/healthz", type_search=SearchType.ABSOLUTE), # OpenEndpoint(path="/healthz", type_search=SearchType.ABSOLUTE),
OpenEndpoint( # OpenEndpoint(
path=settings.ROOT_PATH + "/healthz", # path=settings.ROOT_PATH + "/healthz",
type_search=SearchType.ABSOLUTE, # type_search=SearchType.ABSOLUTE,
), # ),
OpenEndpoint(path="/readyz", type_search=SearchType.ABSOLUTE), # OpenEndpoint(path="/readyz", type_search=SearchType.ABSOLUTE),
OpenEndpoint( # OpenEndpoint(
path=settings.ROOT_PATH + "/readyz", # path=settings.ROOT_PATH + "/readyz",
type_search=SearchType.ABSOLUTE, # type_search=SearchType.ABSOLUTE,
), # ),
OpenEndpoint(path="/openapi.json", type_search=SearchType.START), # OpenEndpoint(path="/openapi.json", type_search=SearchType.START),
OpenEndpoint( # OpenEndpoint(
path=settings.ROOT_PATH + "/openapi.json", # path=settings.ROOT_PATH + "/openapi.json",
type_search=SearchType.START, # type_search=SearchType.START,
), # ),
OpenEndpoint(path="/docs", type_search=SearchType.START), # OpenEndpoint(path="/docs", type_search=SearchType.START),
OpenEndpoint( # OpenEndpoint(
path=settings.ROOT_PATH + "/docs", # path=settings.ROOT_PATH + "/docs",
type_search=SearchType.START, # type_search=SearchType.START,
), # ),
], # ],
) # )
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,

View File

@ -181,7 +181,7 @@ class DepthSheetWriter:
worksheet.column_dimensions[col_letter].width = adjusted_width worksheet.column_dimensions[col_letter].width = adjusted_width
def _set_color_to_row(self, ws: Worksheet, item_type: str) -> None: def _set_color_to_row(self, ws: Worksheet, item_type: str) -> None:
if not self.palette: if not self.palette or item_type in ("SUB_ITEM", "INPUT"):
return return
row = ws.max_row row = ws.max_row

View File

@ -7,6 +7,7 @@ from openpyxl import Workbook
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from src.repository.budget_form_repository import BudgetFormRepository
from src.core.errors import AccessDeniedException, ValidationException from src.core.errors import AccessDeniedException, ValidationException
from src.db.models import AppUser, UserRoleEnum, Vsp from src.db.models import AppUser, UserRoleEnum, Vsp
from src.domain.schemas import VSPCreate, VSPUpdate from src.domain.schemas import VSPCreate, VSPUpdate
@ -29,6 +30,7 @@ class VSPService:
def __init__(self, db: AsyncSession): def __init__(self, db: AsyncSession):
self.db = db self.db = db
self.vsp_repo = VSPRepository(db) self.vsp_repo = VSPRepository(db)
self.form_repo = BudgetFormRepository(db)
self.user_repo = UserRepository(db) self.user_repo = UserRepository(db)
async def get(self, vsp_id: int, user: AppUser, load_org_unit: bool = False) -> Optional[Vsp]: async def get(self, vsp_id: int, user: AppUser, load_org_unit: bool = False) -> Optional[Vsp]:
@ -94,19 +96,27 @@ class VSPService:
self, self,
user: AppUser, user: AppUser,
ssp_id: int | None = None, ssp_id: int | None = None,
form_id: int | None = None,
with_count: bool = False, with_count: 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)
checking_ssps = set()
if ssp_id is not None: if ssp_id is not None:
checking_ssps.add(ssp_id)
if form_id is not None:
form = await self.form_repo.get(budget_form_id=form_id)
if form is not None:
checking_ssps.add(form.org_unit_id)
if checking_ssps:
if ssp_ids is None: if ssp_ids is None:
ssp_ids = [ssp_id] ssp_ids = list(checking_ssps)
elif ssp_id not in set(ssp_ids): elif not checking_ssps & set(ssp_ids):
if with_count: if with_count:
return 0, [] return 0, []
return [] return []
else: else:
ssp_ids = [ssp_id] ssp_ids = list(checking_ssps & set(ssp_ids))
return await self.vsp_repo.get_list( return await self.vsp_repo.get_list(
ssp_ids=ssp_ids, ssp_ids=ssp_ids,

View File

@ -37,14 +37,25 @@ def test_vsp_dropdown_smoke(client, admin_tokens, auth_headers):
assert isinstance(payload["result"], list) assert isinstance(payload["result"], list)
def test_vsp_dropdown_with_ssp_filter(client, admin_tokens, auth_headers): @pytest.mark.parametrize(
"params, count",
[
([("ssp_id", 2)], 1),
([("form_id", 1)], 0),
([("form_id", 2)], 1),
([("ssp_id", 2), ("form_id", 1)], 1),
]
)
def test_vsp_dropdown_with_filter(client, admin_tokens, auth_headers, params, count):
filters = "&".join([f"{el[0]}={el[1]}" for el in params])
response = client.get( response = client.get(
"/api/v1/dict/info/dropdown?ssp_id=1", f"/api/v1/dict/info/dropdown?{filters}",
headers=auth_headers(admin_tokens), headers=auth_headers(admin_tokens),
) )
assert response.status_code == 200 assert response.status_code == 200
payload = response.json() payload = response.json()
assert isinstance(payload["result"], list) assert isinstance(payload["result"], list)
assert len(payload["result"]) == count
def test_vsp_get_by_id(client, admin_tokens, auth_headers): def test_vsp_get_by_id(client, admin_tokens, auth_headers):

View File

@ -15,8 +15,9 @@ INSERT INTO v3.user_org (id,user_id,org_unit_id) VALUES
SELECT setval('v3.user_org_id_seq', 3); SELECT setval('v3.user_org_id_seq', 3);
INSERT INTO v3.budget_form (id,form_type_code,"year",created_by,created_at,updated_by,updated_at,org_unit_id) VALUES INSERT INTO v3.budget_form (id,form_type_code,"year",created_by,created_at,updated_by,updated_at,org_unit_id) VALUES
(1,'FORM_1',2027,NULL,'2026-05-06 16:57:38.066371',NULL,'2026-06-11 14:35:53.783017',1); (1,'FORM_1',2027,NULL,'2026-05-06 16:57:38.066371',NULL,'2026-06-11 14:35:53.783017',1),
SELECT setval('v3.budget_form_id_seq', 1); (4,'FORM_2',2026,NULL,'2026-05-08 09:08:12.230445',NULL,'2026-05-08 09:08:12.230445',2);
SELECT setval('v3.budget_form_id_seq', 4);
INSERT INTO v3.budget_line (id,budget_form_id,expense_item_id,"name",internal_order,vsp_id,project_id,justification,created_by,created_at,updated_by,updated_at,direction) VALUES INSERT INTO v3.budget_line (id,budget_form_id,expense_item_id,"name",internal_order,vsp_id,project_id,justification,created_by,created_at,updated_by,updated_at,direction) VALUES
(1,1,88,'1234',NULL,NULL,NULL,NULL,NULL,'2026-05-06 16:58:05.793083',NULL,'2026-06-22 17:41:54.255313','Support'); (1,1,88,'1234',NULL,NULL,NULL,NULL,NULL,'2026-05-06 16:58:05.793083',NULL,'2026-06-22 17:41:54.255313','Support');
@ -26,5 +27,6 @@ INSERT INTO v3.form_phase (budget_form_id,sheet,phase_code,"role",column_keys,op
(1,'AHR','test','DFIP','{{plan.q1}}','2026-05-05 03:00:00+03','2026-06-06 03:00:00+03'); (1,'AHR','test','DFIP','{{plan.q1}}','2026-05-05 03:00:00+03','2026-06-06 03:00:00+03');
INSERT INTO v3.vsp (id,branch_id,reg_number,address,format,opened_at,placement_type,staff_count,total_area,closed_at,is_active,updated_at,is_deleted,created_at,created_by,system_code,updated_by,vsp_type,notes,location_form,numbers,rent_contract_num,rent_end_date) VALUES INSERT INTO v3.vsp (id,branch_id,reg_number,address,format,opened_at,placement_type,staff_count,total_area,closed_at,is_active,updated_at,is_deleted,created_at,created_by,system_code,updated_by,vsp_type,notes,location_form,numbers,rent_contract_num,rent_end_date) VALUES
(1,2,'Тестовый всп 1','Тестовая 12','укукк','2026-05-07','ывс',2026,230,'2026-06-16',false,'2026-06-18 15:46:38.611903',false,'2026-06-04 14:36:59.737727',1,'1233',91,'','','встроенное помещение',NULL,'',NULL); (1,2,'Тестовый всп 1','Тестовая 12','укукк','2026-05-07','ывс',2026,230,'2026-06-16',false,'2026-06-18 15:46:38.611903',false,'2026-06-04 14:36:59.737727',1,'1233',91,'','','встроенное помещение',NULL,'',NULL),
SELECT setval('v3.vsp_id_seq', 1); (2,2,'Test12 -----','Тестовая 125','формат 15 ','2026-06-08',NULL,NULL,58,NULL,true,'2026-06-16 18:53:17.986651',false,'2026-06-09 09:04:33.29858',91,'123-1234',91,'субаренда','Работает ','аренда',155,'12-45','2026-10-02');
SELECT setval('v3.vsp_id_seq', 2);

View File

@ -143,7 +143,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
} }
}, },
columnVirtualizerOptions: ({ table }) => ({ columnVirtualizerOptions: ({ table }) => ({
overscan: 1, overscan: 10,
measureElement: (el) => { measureElement: (el) => {
if (!el) return 150; if (!el) return 150;
const index = Number(el?.getAttribute?.('data-index')); const index = Number(el?.getAttribute?.('data-index'));
@ -152,7 +152,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
const colId = table.getState().columnPinning.left[index]; const colId = table.getState().columnPinning.left[index];
const column = table.getColumn(colId); const column = table.getColumn(colId);
return column.columnDef.size; return column.columnDef.size;
} }
const allCols = [...table.getLeftVisibleLeafColumns(), ...table.getCenterVisibleLeafColumns()] const allCols = [...table.getLeftVisibleLeafColumns(), ...table.getCenterVisibleLeafColumns()]
return allCols[index]?.getSize() ?? 150; return allCols[index]?.getSize() ?? 150;

View File

@ -168,7 +168,7 @@ const SettingPanel = ({
<ExportDefaultButton <ExportDefaultButton
onClick={handleExport} onClick={handleExport}
disabled={!formId || !sheetName || isExporting} disabled={!formId || !sheetName || isExporting}
text={isExporting ? 'Экспорт...' : 'Экспорт в Excel'} text={isExporting ? 'Экспорт...' : 'Экспорт'}
/> />
</GroupByObject> </GroupByObject>
</Stack> </Stack>

View File

@ -38,13 +38,12 @@ export const AddStagesModal = ({
sheetOptions = [], // [{ value: 'sheet1', label: 'Лист 1' }] sheetOptions = [], // [{ value: 'sheet1', label: 'Лист 1' }]
formType = '', formType = '',
defaultSheet = '', defaultSheet = '',
defaultRole,
}) => { }) => {
const [startDate, setStartDate] = useState(null); const [startDate, setStartDate] = useState(null);
const [endDate, setEndDate] = useState(null); const [endDate, setEndDate] = useState(null);
const [stageName, setStageName] = useState(''); const [stageName, setStageName] = useState('');
const [selectedSheet, setSelectedSheet] = useState(defaultSheet); const [selectedSheet, setSelectedSheet] = useState(defaultSheet);
const [selectedRole, setSelectedRole] = useState(defaultRole); const [selectedRole, setSelectedRole] = useState();
const [columnsConfig, setColumnsConfig] = useState({ columns: [], colors: {} }); const [columnsConfig, setColumnsConfig] = useState({ columns: [], colors: {} });
const [selectedColumns, setSelectedColumns] = useState([]); const [selectedColumns, setSelectedColumns] = useState([]);
@ -89,10 +88,10 @@ export const AddStagesModal = ({
setEndDate(null); setEndDate(null);
setStageName(''); setStageName('');
setSelectedSheet(defaultSheet); setSelectedSheet(defaultSheet);
setSelectedRole(defaultRole); setSelectedRole();
setSelectedColumns([]); setSelectedColumns([]);
} }
}, [stage, isOpen, defaultSheet, defaultRole]); }, [stage, isOpen, defaultSheet]);
const handleSave = () => { const handleSave = () => {
const opensAt = formatDateForApi(startDate); const opensAt = formatDateForApi(startDate);
@ -129,7 +128,7 @@ export const AddStagesModal = ({
setEndDate(null); setEndDate(null);
setStageName(''); setStageName('');
setSelectedSheet(defaultSheet); setSelectedSheet(defaultSheet);
setSelectedRole(defaultRole); setSelectedRole();
setSelectedColumns([]); setSelectedColumns([]);
onClose(); onClose();
}; };
@ -238,6 +237,7 @@ export const AddStagesModal = ({
placeholderSelect="Выбрано столбцов: " placeholderSelect="Выбрано столбцов: "
selectedIds={selectedColumns} selectedIds={selectedColumns}
onSelectNode={handleColumnsSelect} onSelectNode={handleColumnsSelect}
forStage={true}
/> />
</FormControl> </FormControl>
</Stack> </Stack>
@ -247,7 +247,7 @@ export const AddStagesModal = ({
<FormControl fullWidth> <FormControl fullWidth>
<FormLabel>Роль</FormLabel> <FormLabel>Роль</FormLabel>
<Select <Select
value={selectedRole} value={selectedRole ?? ''}
onChange={(e) => setSelectedRole(e.target.value)} onChange={(e) => setSelectedRole(e.target.value)}
displayEmpty displayEmpty
size="small" size="small"

View File

@ -76,41 +76,59 @@ export const SettingModal = ({
setIsAddEditModalOpen(true); setIsAddEditModalOpen(true);
}; };
const transformToSheetOptions = (data) => { const transformToSheetOptions = (data) => {
const sheetMap = new Map(); // TO DO пока не добавили direction для этапов
data.forEach(item => { // const sheetMap = new Map();
const key = item.direction ? `${item.sheet}_${item.direction}` : item.sheet;
if (!sheetMap.has(key)) { // data.forEach(item => {
sheetMap.set(key, { // const key = item.direction ? `${item.sheet}_${item.direction}` : item.sheet;
sheet: item.sheet,
direction: item.direction,
names: []
});
}
const entry = sheetMap.get(key); // if (!sheetMap.has(key)) {
if (!entry.names.includes(item.name)) { // sheetMap.set(key, {
entry.names.push(item.name); // sheet: item.sheet,
} // direction: item.direction,
}); // names: []
// });
// }
// const entry = sheetMap.get(key);
// if (!entry.names.includes(item.name)) {
// entry.names.push(item.name);
// }
// });
// const sheetOptions = [];
// sheetMap.forEach((value) => {
// let label = value.names.join(' / ');
// if (value.direction) {
// label += ` (${value.direction})`;
// }
// sheetOptions.push({
// value: value.direction ? `${value.sheet}_${value.direction}` : value.sheet,
// label: label,
// sheet: value.sheet,
// direction: value.direction
// });
// });
// return sheetOptions;
const sheetOptions = []; const sheetOptions = [];
data.forEach((value) => {
sheetMap.forEach((value) => { if (sheetOptions.find(s => s.sheet == value.sheet)) {
let label = value.names.join(' / '); return;
if (value.direction) {
label += ` (${value.direction})`;
} }
sheetOptions.push({ sheetOptions.push({
value: value.direction ? `${value.sheet}_${value.direction}` : value.sheet, value: value.sheet,
label: label, label: value.name,
sheet: value.sheet, sheet: value.sheet,
direction: value.direction direction: value.direction,
}); })
}); })
return sheetOptions; return sheetOptions;
}; };

View File

@ -34,7 +34,7 @@ export const ExportWithTextButton = (props) => {
<Stack sx={{ flexDirection: 'row', alignItems: 'center', gap: '.375rem' }}> <Stack sx={{ flexDirection: 'row', alignItems: 'center', gap: '.375rem' }}>
<ExportSvg size="1rem" color="#45881c" strokeWidth={'.33px'} /> <ExportSvg size="1rem" color="#45881c" strokeWidth={'.33px'} />
<span style={{ fontSize: '.88rem' }}> <span style={{ fontSize: '.88rem' }}>
{props?.text || 'Экспорт в Excel'} {props?.text || 'Экспорт'}
</span> </span>
</Stack> </Stack>
</PrimaryOutlinedButton> </PrimaryOutlinedButton>
@ -57,7 +57,7 @@ export const ExportDefaultButton = (props) => {
<DefaultOutlinedButton {...props}> <DefaultOutlinedButton {...props}>
<Stack sx={{ flexDirection: 'row', alignItems: 'center', gap: '.375rem' }}> <Stack sx={{ flexDirection: 'row', alignItems: 'center', gap: '.375rem' }}>
<ExportSvg color="#4a5565" strokeWidth={'.33'} size="1rem" /> <ExportSvg color="#4a5565" strokeWidth={'.33'} size="1rem" />
<span>{props?.text || 'Экспорт в Excel'}</span> <span>{props?.text || 'Экспорт'}</span>
</Stack> </Stack>
</DefaultOutlinedButton> </DefaultOutlinedButton>
); );

View File

@ -21,6 +21,7 @@ const CollapsibleTree = ({
placeholderSelect = 'Скрыто столбцов: ', placeholderSelect = 'Скрыто столбцов: ',
selectedIds = [], selectedIds = [],
onSelectNode, onSelectNode,
forStage = false
}) => { }) => {
const [expandedItems, setExpandedItems] = useState([]); const [expandedItems, setExpandedItems] = useState([]);
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
@ -144,7 +145,7 @@ const CollapsibleTree = ({
setAnchorEl(null); setAnchorEl(null);
}; };
const selectedCount = selectedNodes.length - 1; //1 колонка всегда скрыта const selectedCount = forStage ? selectedNodes.length : selectedNodes.length - 1; //1 колонка всегда скрыта
const isAllSelected = () => { const isAllSelected = () => {
if (!columnTree) return false; if (!columnTree) return false;

View File

@ -1,3 +1,4 @@
import { DIRECTION_TRANSLATE, FORM_TYPE_OPTIONS } from '../../../constants';
import { import {
Section, Section,
SectionStartPart, SectionStartPart,
@ -22,7 +23,7 @@ const TableCard = ({ table, onNavigate }) => {
</div> </div>
<SectionTitle> <SectionTitle>
<h2>{table.name}</h2> <h2>{table.name}</h2>
<h2>{table?.direction}</h2> <h2>{DIRECTION_TRANSLATE[table?.direction]}</h2>
{/* <UsersList> {/* <UsersList>
<div className="img-user"></div> <div className="img-user"></div>
</UsersList> */} </UsersList> */}

View File

@ -37,6 +37,7 @@ export const EDIT_ACCESS_RUSSIAN = {
export const FORM_TYPE_OPTIONS = ['FORM_1', 'FORM_2', 'FORM_3', 'FORM_4']; export const FORM_TYPE_OPTIONS = ['FORM_1', 'FORM_2', 'FORM_3', 'FORM_4'];
export const FROM_TYPE_TRANSLATE = { 'FORM_1': 'Смета ССП', 'FORM_2': 'Смета РФ', 'FORM_3': 'Программа развитие ПРРС', 'FORM_4': 'Проектная деятельность' }; export const FROM_TYPE_TRANSLATE = { 'FORM_1': 'Смета ССП', 'FORM_2': 'Смета РФ', 'FORM_3': 'Программа развитие ПРРС', 'FORM_4': 'Проектная деятельность' };
export const DIRECTION_TRANSLATE = { 'Support': 'Поддержка', 'Development': 'Развитие' };
export const ORG_UNIT_TYPE_OPTIONS = [ export const ORG_UNIT_TYPE_OPTIONS = [
{ value: 'ssp', label: 'ССП' }, { value: 'ssp', label: 'ССП' },

View File

@ -22,7 +22,7 @@ import { ModalCreateProject } from './ModalCreateProject';
import { ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions'; import { ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions';
import { exportSingleForm } from '../../utils/exportForm'; import { exportSingleForm } from '../../utils/exportForm';
import TablesList from '../../components/common/TableList/TableList'; import TablesList from '../../components/common/TableList/TableList';
import { FROM_TYPE_TRANSLATE, SHEET_NAME } from '../../constants'; import { DIRECTION_TRANSLATE, FROM_TYPE_TRANSLATE, SHEET_NAME } from '../../constants';
const TaskInfo = ({ task }) => { const TaskInfo = ({ task }) => {
const portalContent = ( const portalContent = (
@ -61,9 +61,10 @@ export default function TaskPage() {
const mappedTables = data.result.all_sheets.map(sheet => ({ const mappedTables = data.result.all_sheets.map(sheet => ({
name: SHEET_NAME[sheet.sheet_name] || sheet.sheet_name, name: SHEET_NAME[sheet.sheet_name] || sheet.sheet_name,
sheet: sheet.sheet_name, sheet: sheet.sheet_name,
direction: sheet.direction direction: sheet.direction,
})); }));
setTables(mappedTables); //пока скрыт
setTables(mappedTables.filter(t => t.sheet !== "OTCH9F",));
} catch (error) { } catch (error) {
toast.error( toast.error(
error?.response?.data?.detail || 'Ошибка получения данных задачи', error?.response?.data?.detail || 'Ошибка получения данных задачи',

View File

@ -26,6 +26,7 @@ import {
FORM_TYPE_OPTIONS, FORM_TYPE_OPTIONS,
ORG_UNIT_TYPE_OPTIONS, ORG_UNIT_TYPE_OPTIONS,
ROLES_NAME_ID, ROLES_NAME_ID,
FROM_TYPE_TRANSLATE
} from '../../constants'; } from '../../constants';
import { useAuth } from '../../app/context/AuthProvider'; import { useAuth } from '../../app/context/AuthProvider';
import { SspApi } from '../../api/ssp'; import { SspApi } from '../../api/ssp';
@ -129,8 +130,8 @@ export default function TasksPage() {
} catch (error) { } catch (error) {
toast.error( toast.error(
error.response?.data?.detail || error.response?.data?.detail ||
error.response?.data?.message || error.response?.data?.message ||
'Ошибка при создании задачи', 'Ошибка при создании задачи',
); );
} finally { } finally {
setLoadingUpdate(false); setLoadingUpdate(false);
@ -182,7 +183,7 @@ export default function TasksPage() {
const res = await TasksApi.list(params); const res = await TasksApi.list(params);
const data = res.result; const data = res.result;
setItems(data); setItems(data);
setTotalCount(res.count); setTotalCount(res.count);
@ -413,7 +414,7 @@ export default function TasksPage() {
onChange={(e) => handleFormChange('formTypeCode', e.target.value)} onChange={(e) => handleFormChange('formTypeCode', e.target.value)}
renderValue={(selected) => renderValue={(selected) =>
selected ? ( selected ? (
selected FROM_TYPE_TRANSLATE[selected]
) : ( ) : (
<span style={{ color: 'rgba(0,0,0,0.6)' }}>Не выбрана</span> <span style={{ color: 'rgba(0,0,0,0.6)' }}>Не выбрана</span>
) )
@ -423,7 +424,7 @@ export default function TasksPage() {
> >
{FORM_TYPE_OPTIONS.map((formType) => ( {FORM_TYPE_OPTIONS.map((formType) => (
<MenuItem key={formType} value={formType}> <MenuItem key={formType} value={formType}>
{formType} {FROM_TYPE_TRANSLATE[formType]}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>