delete-projects: удаление проектов, фикс редактируемых полей в форме 4, нельзя удалить самого себя, нельзя выбрать неактивный ссп для экспорта всп, фикс для показа доступных полей для редактирования (бэк)
This commit is contained in:
parent
5498bf4935
commit
d3ccc21c35
84
api/alembic/versions/0012_del_project.py
Normal file
84
api/alembic/versions/0012_del_project.py
Normal file
@ -0,0 +1,84 @@
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "0012"
|
||||
down_revision = "0011"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
_DOLLAR_TAG_RE = re.compile(r"\$\w+\$")
|
||||
|
||||
|
||||
def _find_dollar_tag(line: str) -> str | None:
|
||||
m = _DOLLAR_TAG_RE.search(line.strip())
|
||||
return m.group(0) if m else None
|
||||
|
||||
|
||||
def _split_statements(sql: str) -> list[str]:
|
||||
statements: list[str] = []
|
||||
current: list[str] = []
|
||||
in_dollar = False
|
||||
dollar_tag: str | None = None
|
||||
|
||||
for line in sql.split("\n"):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("--"):
|
||||
continue
|
||||
|
||||
if not in_dollar:
|
||||
tag = _find_dollar_tag(stripped)
|
||||
if tag and tag.endswith("$") and tag.startswith("$"):
|
||||
dollar_tag = tag
|
||||
in_dollar = True
|
||||
current.append(line)
|
||||
continue
|
||||
|
||||
if in_dollar and dollar_tag and stripped.startswith(dollar_tag):
|
||||
after = stripped[len(dollar_tag):].strip()
|
||||
if after == ";" or after == "":
|
||||
in_dollar = False
|
||||
dollar_tag = None
|
||||
if after == ";":
|
||||
current.append(line)
|
||||
statements.append("\n".join(current))
|
||||
current = []
|
||||
continue
|
||||
|
||||
if not in_dollar and stripped.rstrip().endswith(";"):
|
||||
current.append(line)
|
||||
statements.append("\n".join(current))
|
||||
current = []
|
||||
continue
|
||||
|
||||
current.append(line)
|
||||
|
||||
remaining = "\n".join(current).strip()
|
||||
if remaining:
|
||||
statements.append(remaining)
|
||||
|
||||
return statements
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
ddl_path = os.path.join(os.path.dirname(__file__), "sql", "0012_del_project.sql")
|
||||
with open(ddl_path) as f:
|
||||
content = f.read()
|
||||
|
||||
statements = _split_statements(content)
|
||||
for stmt in statements:
|
||||
stripped = stmt.strip().rstrip(";").strip()
|
||||
if not stripped:
|
||||
continue
|
||||
if all(l.strip().startswith("--") or not l.strip() for l in stripped.split("\n")):
|
||||
continue
|
||||
op.execute(stripped)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
2464
api/alembic/versions/sql/0012_del_project.sql
Normal file
2464
api/alembic/versions/sql/0012_del_project.sql
Normal file
File diff suppressed because one or more lines are too long
@ -15,6 +15,7 @@ from src.domain.schemas import (
|
||||
BaseSingleResponse,
|
||||
CellPatch,
|
||||
CellsPatch,
|
||||
ResponseBase,
|
||||
SheetResponse,
|
||||
UpdProjectBody,
|
||||
)
|
||||
@ -251,6 +252,17 @@ async def upd_project(
|
||||
return result
|
||||
|
||||
|
||||
@router.delete("/projects/{project_id}")
|
||||
async def delete_project(
|
||||
project_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||
) -> ResponseBase:
|
||||
project_service = ProjectService(db)
|
||||
await project_service.delete_project(project_id=project_id, user=current_user)
|
||||
return ResponseBase(success=True, message="Проект удалён")
|
||||
|
||||
|
||||
@router.post("/projects")
|
||||
async def add_project(
|
||||
body: AddProjectBody,
|
||||
|
||||
@ -232,12 +232,14 @@ class ConnectionManager:
|
||||
form_key: dict,
|
||||
) -> bool:
|
||||
if not cell_key:
|
||||
print("not cell key")
|
||||
return False
|
||||
cell_key = frozenset(cell_key.items())
|
||||
|
||||
lock_key = (con_key, frozenset(form_key.items()), cell_key)
|
||||
lock_owner_id = self.cell_locks.get(lock_key)
|
||||
if lock_owner_id is not None and lock_owner_id != user_id:
|
||||
print(lock_owner_id, user_id)
|
||||
return False
|
||||
self.cell_locks[lock_key] = user_id
|
||||
return True
|
||||
@ -283,7 +285,11 @@ class ConnectionManager:
|
||||
if not cell_key:
|
||||
return True
|
||||
cell_key = frozenset(cell_key.items())
|
||||
print(cell_key)
|
||||
print(self.cell_locks)
|
||||
lock_owner_id = self.cell_locks.get((con_key, frozenset(form_key.items()), cell_key))
|
||||
print(1)
|
||||
print(lock_owner_id, user_id)
|
||||
return lock_owner_id is not None and lock_owner_id == user_id
|
||||
|
||||
def is_row_locked_by_other(
|
||||
@ -707,10 +713,10 @@ async def login(websocket: WebSocket, **kwargs) -> int:
|
||||
def resolve_cell_key(event_data: dict) -> dict | None:
|
||||
data = event_data.get("data")
|
||||
|
||||
if not data or "line_id" not in data or "column" not in data:
|
||||
if not data or ("line_id" not in data and "line_id_code" not in data) or "column" not in data:
|
||||
return None
|
||||
return {
|
||||
"line_id": data.get("line_id_code", data["line_id"]),
|
||||
"line_id": data.get("line_id_code") or data.get("line_id"),
|
||||
"column": data["column"],
|
||||
}
|
||||
|
||||
@ -751,6 +757,7 @@ async def process_websocket(
|
||||
try:
|
||||
event_name = data.get("event")
|
||||
cell_key = resolve_cell_key(data)
|
||||
print(cell_key, "da")
|
||||
|
||||
match event_name:
|
||||
case "cell_edit_start":
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import CheckConstraint, ForeignKey, Index, Integer, Numeric, String, text
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Index, Integer, Numeric, String, func, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src.db.base import Base
|
||||
@ -41,6 +43,7 @@ class Project(Base):
|
||||
staff_count: Mapped[int | None] = mapped_column(Integer)
|
||||
total_area: Mapped[float | None] = mapped_column(Numeric)
|
||||
org_unit_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("v3.org_unit.id"))
|
||||
created_at: Mapped[datetime | None] = mapped_column(DateTime, server_default=func.now())
|
||||
|
||||
# parent = relationship("Project", remote_side="Project.id", back_populates="children")
|
||||
# children: Mapped[list["Project"]] = relationship("Project", back_populates="parent") # type: ignore
|
||||
|
||||
@ -28,6 +28,7 @@ class ProjectRepository:
|
||||
"total_area": float(project.total_area) if project.total_area is not None else None,
|
||||
"org_unit_id": project.org_unit_id,
|
||||
"org_unit_name": org_unit_name,
|
||||
"created_at": project.created_at.isoformat() if project.created_at else None,
|
||||
"report_count": int(report_count),
|
||||
"years": years,
|
||||
}
|
||||
@ -316,6 +317,15 @@ class ProjectRepository:
|
||||
rows = (await self.db.execute(query, {"line_id": line_id})).all()
|
||||
return [tuple(r) for r in rows]
|
||||
|
||||
async def del_project(self, project_id: int):
|
||||
query = text(
|
||||
"""
|
||||
SELECT v3.del_project(CAST(:project_id AS INT))
|
||||
"""
|
||||
)
|
||||
result = (await self.db.execute(query, {"project_id": project_id})).scalar_one()
|
||||
return result
|
||||
|
||||
async def upd_project(self, project_id: int, column: str, value):
|
||||
query = text(
|
||||
"""
|
||||
|
||||
@ -147,6 +147,12 @@ class ProjectService:
|
||||
raise ValidationException("Отчёт не найден")
|
||||
return await self.project_repo.del_form3_line(line_id)
|
||||
|
||||
async def delete_project(self, project_id: int, user: AppUser):
|
||||
project = await self.get(project_id, user)
|
||||
if not project:
|
||||
raise ValidationException("Проект не найден")
|
||||
return await self.project_repo.del_project(project_id)
|
||||
|
||||
async def upd_project(
|
||||
self,
|
||||
project_id: int,
|
||||
|
||||
@ -85,6 +85,11 @@ class UserService:
|
||||
)
|
||||
update_payload["role_id"] = role.value
|
||||
|
||||
if user.id == user_id and not update_payload.get('is_active', True):
|
||||
raise ValidationException(
|
||||
"Нельзя удалить себя", field="role_id"
|
||||
)
|
||||
|
||||
if "username" in update_payload and update_payload["username"] != target.username:
|
||||
conflict = await self.user_repo.get_by_username(update_payload["username"])
|
||||
if conflict:
|
||||
@ -122,6 +127,10 @@ class UserService:
|
||||
async def delete_user(self, user_id: int, user: AppUser):
|
||||
if user.role_id != UserRoleEnum.ADMIN:
|
||||
raise AccessDeniedException()
|
||||
if user.id == user_id:
|
||||
raise ValidationException(
|
||||
"Нельзя удалить себя", field="role_id"
|
||||
)
|
||||
deleted = await self.user_repo.logical_delete(user_id)
|
||||
if not deleted:
|
||||
raise UserNotFoundException()
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
@ -12,11 +13,11 @@ def test_projects_list_smoke(client, admin_tokens, auth_headers):
|
||||
assert isinstance(payload["result"], list)
|
||||
|
||||
|
||||
def _create_project(client, admin_tokens, auth_headers) -> tuple[int, str]:
|
||||
def _create_project(client, admin_tokens, auth_headers, year=2026, branch_id=1) -> tuple[int, str]:
|
||||
name = f"PT_{uuid.uuid4().hex[:10]}"
|
||||
response = client.post(
|
||||
"/api/v1/projects",
|
||||
json={"name": name, "year": 2026, "branch_id": 1},
|
||||
json={"name": name, "year": year, "branch_id": branch_id},
|
||||
headers=auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@ -24,10 +25,10 @@ def _create_project(client, admin_tokens, auth_headers) -> tuple[int, str]:
|
||||
return payload["project_id"], name
|
||||
|
||||
|
||||
def _add_line(client, admin_tokens, auth_headers, project_id: int) -> int:
|
||||
def _add_line(client, admin_tokens, auth_headers, project_id: int, year: int = 2026) -> int:
|
||||
for expense_item_id in range(1, 80):
|
||||
response = client.post(
|
||||
f"/api/v1/projects/{project_id}/report/2026/LIMIT/line",
|
||||
f"/api/v1/projects/{project_id}/report/{year}/LIMIT/line",
|
||||
json={"expense_item_id": expense_item_id},
|
||||
headers=auth_headers(admin_tokens),
|
||||
)
|
||||
@ -143,3 +144,107 @@ def test_projects_write_report_not_found_error_shape(client, admin_tokens, auth_
|
||||
payload = response.json()
|
||||
assert isinstance(payload, dict)
|
||||
assert payload.get("message") == "Отчёт не найден"
|
||||
|
||||
|
||||
def test_delete_project_smoke(client, admin_tokens, auth_headers):
|
||||
project_id, _ = _create_project(client, admin_tokens, auth_headers, year=datetime.now().year)
|
||||
|
||||
response = client.delete(
|
||||
f"/api/v1/projects/{project_id}",
|
||||
headers=auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["success"] is True
|
||||
assert payload["message"] == "Проект удалён"
|
||||
|
||||
|
||||
def test_delete_project_verify_gone(client, admin_tokens, auth_headers):
|
||||
project_id, _ = _create_project(client, admin_tokens, auth_headers, year=datetime.now().year)
|
||||
|
||||
del_response = client.delete(
|
||||
f"/api/v1/projects/{project_id}",
|
||||
headers=auth_headers(admin_tokens),
|
||||
)
|
||||
assert del_response.status_code == 200
|
||||
|
||||
get_response = client.get(
|
||||
f"/api/v1/projects/{project_id}",
|
||||
headers=auth_headers(admin_tokens),
|
||||
)
|
||||
assert get_response.status_code == 404
|
||||
|
||||
|
||||
def test_delete_project_not_found(client, admin_tokens, auth_headers):
|
||||
response = client.delete(
|
||||
"/api/v1/projects/99999",
|
||||
headers=auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 400
|
||||
payload = response.json()
|
||||
assert isinstance(payload, dict)
|
||||
assert "message" in payload
|
||||
|
||||
|
||||
def test_delete_project_unauthorized(client):
|
||||
response = client.delete("/api/v1/projects/1")
|
||||
assert response.status_code == 403
|
||||
|
||||
#ToDo: вернуть, когда пофиксим смещение фаз в зависимости от года
|
||||
# def test_delete_project_with_lines_blocked(client, admin_tokens, auth_headers):
|
||||
# """Проект со строками отчёта нельзя удалить: can_delete_project → false → RAISE EXCEPTION."""
|
||||
# year = datetime.now().year - 1
|
||||
# project_id, _ = _create_project(client, admin_tokens, auth_headers, year=year)
|
||||
# _add_line(client, admin_tokens, auth_headers, project_id, year=year)
|
||||
|
||||
# response = client.delete(
|
||||
# f"/api/v1/projects/{project_id}",
|
||||
# headers=auth_headers(admin_tokens),
|
||||
# )
|
||||
# assert response.status_code == 400
|
||||
# payload = response.json()
|
||||
# assert isinstance(payload, dict)
|
||||
# assert "code" in payload
|
||||
# assert "message" in payload
|
||||
# assert "невозможно удалить" in payload["message"]
|
||||
|
||||
|
||||
def test_delete_project_executor_from_other_org(client, admin_tokens, auth_headers, isp_tokens):
|
||||
"""ISP1 (привязан к РФ id=2) не может удалить проект на ССП id=1 — ACL."""
|
||||
project_id, _ = _create_project(client, admin_tokens, auth_headers, branch_id=1, year=datetime.now().year)
|
||||
|
||||
response = client.delete(
|
||||
f"/api/v1/projects/{project_id}",
|
||||
headers=auth_headers(isp_tokens),
|
||||
)
|
||||
assert response.status_code == 400
|
||||
payload = response.json()
|
||||
assert "message" in payload
|
||||
assert payload["message"] == "Проект не найден"
|
||||
|
||||
|
||||
#ToDo: вернуть, когда пофиксим смещение фаз в зависимости от года
|
||||
# def test_delete_project_executor_from_same_org(client, isp_tokens, auth_headers):
|
||||
# """ISP1 может удалить проект на своём РФ (id=2), если нет строк отчёта."""
|
||||
# project_id = _create_project(client, isp_tokens, auth_headers, branch_id=2, year=datetime.now().year)
|
||||
|
||||
# response = client.delete(
|
||||
# f"/api/v1/projects/{project_id}",
|
||||
# headers=auth_headers(isp_tokens),
|
||||
# )
|
||||
# assert response.status_code == 200
|
||||
# payload = response.json()
|
||||
# assert payload["success"] is True
|
||||
# assert payload["message"] == "Проект удалён"
|
||||
|
||||
|
||||
def test_delete_seed_project_smoke(client, admin_tokens, auth_headers):
|
||||
"""Проект из фикстуры (id=2) удаляется: у него нет строк отчёта, can_delete_project → true."""
|
||||
response = client.delete(
|
||||
"/api/v1/projects/2",
|
||||
headers=auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["success"] is True
|
||||
assert payload["message"] == "Проект удалён"
|
||||
|
||||
@ -46,9 +46,9 @@
|
||||
"color_type":greenColumn,
|
||||
"accessorKey":"data.plan.comment"
|
||||
},
|
||||
"data.contract_summary.amount":{
|
||||
"data.contract_summary.total":{
|
||||
"color_type":greenColumn,
|
||||
"accessorKey":"data.contract_summary.amount"
|
||||
"accessorKey":"data.contract_summary.total"
|
||||
},
|
||||
"data.contract_summary.reference":{
|
||||
"color_type":greenColumn,
|
||||
@ -66,17 +66,17 @@
|
||||
"color_type":greenColumn,
|
||||
"accessorKey":"data.contract_summary.comment"
|
||||
},
|
||||
"data.contract_summary.future_payments_y1":{
|
||||
"data.contract_summary.future_y1":{
|
||||
"color_type":greenColumn,
|
||||
"accessorKey":"data.contract_summary.future_payments_y1"
|
||||
"accessorKey":"data.contract_summary.future_y1"
|
||||
},
|
||||
"data.contract_summary.future_payments_y2":{
|
||||
"data.contract_summary.future_y2":{
|
||||
"color_type":greenColumn,
|
||||
"accessorKey":"data.contract_summary.future_payments_y2"
|
||||
"accessorKey":"data.contract_summary.future_y2"
|
||||
},
|
||||
"data.contract_summary.other_ssp_amount":{
|
||||
"data.contract_summary.other_ssp":{
|
||||
"color_type":greenColumn,
|
||||
"accessorKey":"data.contract_summary.other_ssp_amount"
|
||||
"accessorKey":"data.contract_summary.other_ssp"
|
||||
},
|
||||
"data.contract_summary.centralized_flag":{
|
||||
"color_type":greenColumn,
|
||||
@ -158,13 +158,13 @@
|
||||
"color_type":greenColumn,
|
||||
"accessorKey":"data.approved.year"
|
||||
},
|
||||
"data.collegial.amount":{
|
||||
"data.collegial.approved":{
|
||||
"color_type":orangeColumn,
|
||||
"accessorKey":"data.collegial.amount"
|
||||
"accessorKey":"data.collegial.approved"
|
||||
},
|
||||
"data.collegial.protocol_reference":{
|
||||
"data.collegial.protocol":{
|
||||
"color_type":orangeColumn,
|
||||
"accessorKey":"data.collegial.protocol_reference"
|
||||
"accessorKey":"data.collegial.protocol"
|
||||
},
|
||||
"field_kollegialnye_organy_field":{
|
||||
"color_type":orangeColumn,
|
||||
@ -182,37 +182,37 @@
|
||||
"color_type":orangeColumn,
|
||||
"accessorKey":"data.ckk.ceiling"
|
||||
},
|
||||
"data.ckk.expenses_q1":{
|
||||
"data.ckk.q1":{
|
||||
"color_type":orangeColumn,
|
||||
"accessorKey":"data.ckk.expenses_q1"
|
||||
"accessorKey":"data.ckk.q1"
|
||||
},
|
||||
"data.ckk.expenses_q2":{
|
||||
"data.ckk.q2":{
|
||||
"color_type":orangeColumn,
|
||||
"accessorKey":"data.ckk.expenses_q2"
|
||||
"accessorKey":"data.ckk.q2"
|
||||
},
|
||||
"data.ckk.expenses_q3":{
|
||||
"data.ckk.q3":{
|
||||
"color_type":orangeColumn,
|
||||
"accessorKey":"data.ckk.expenses_q3"
|
||||
"accessorKey":"data.ckk.q3"
|
||||
},
|
||||
"data.ckk.expenses_q4":{
|
||||
"data.ckk.q4":{
|
||||
"color_type":orangeColumn,
|
||||
"accessorKey":"data.ckk.expenses_q4"
|
||||
"accessorKey":"data.ckk.q4"
|
||||
},
|
||||
"data.ckk.rf_schedule":{
|
||||
"color_type":orangeColumn,
|
||||
"accessorKey":"data.ckk.rf_schedule"
|
||||
},
|
||||
"data.ckk.delivery_deadline":{
|
||||
"data.ckk.deadline":{
|
||||
"color_type":orangeColumn,
|
||||
"accessorKey":"data.ckk.delivery_deadline"
|
||||
"accessorKey":"data.ckk.deadline"
|
||||
},
|
||||
"data.ckk.procurement_plan":{
|
||||
"data.ckk.proc_plan":{
|
||||
"color_type":orangeColumn,
|
||||
"accessorKey":"data.ckk.procurement_plan"
|
||||
"accessorKey":"data.ckk.proc_plan"
|
||||
},
|
||||
"data.ckk.procurement_method":{
|
||||
"data.ckk.proc_method":{
|
||||
"color_type":orangeColumn,
|
||||
"accessorKey":"data.ckk.procurement_method"
|
||||
"accessorKey":"data.ckk.proc_method"
|
||||
},
|
||||
"data.ckk.comment":{
|
||||
"color_type":orangeColumn,
|
||||
@ -242,21 +242,21 @@
|
||||
"color_type":orangeColumn,
|
||||
"accessorKey":"data.contract.ceiling"
|
||||
},
|
||||
"data.contract.expenses_q1":{
|
||||
"data.contract.q1":{
|
||||
"color_type":orangeColumn,
|
||||
"accessorKey":"data.contract.expenses_q1"
|
||||
"accessorKey":"data.contract.q1"
|
||||
},
|
||||
"data.contract.expenses_q2":{
|
||||
"data.contract.q2":{
|
||||
"color_type":orangeColumn,
|
||||
"accessorKey":"data.contract.expenses_q2"
|
||||
"accessorKey":"data.contract.q2"
|
||||
},
|
||||
"data.contract.expenses_q3":{
|
||||
"data.contract.q3":{
|
||||
"color_type":orangeColumn,
|
||||
"accessorKey":"data.contract.expenses_q3"
|
||||
"accessorKey":"data.contract.q3"
|
||||
},
|
||||
"data.contract.expenses_q4":{
|
||||
"data.contract.q4":{
|
||||
"color_type":orangeColumn,
|
||||
"accessorKey":"data.contract.expenses_q4"
|
||||
"accessorKey":"data.contract.q4"
|
||||
},
|
||||
"data.contract.rf_schedule":{
|
||||
"color_type":orangeColumn,
|
||||
@ -955,7 +955,7 @@
|
||||
"columns":[
|
||||
{
|
||||
"header":"Сумма",
|
||||
"accessorKey":"data.contract_summary.amount",
|
||||
"accessorKey":"data.contract_summary.total",
|
||||
"columnLetter":"L",
|
||||
"size":150,
|
||||
"filterFn":"contains",
|
||||
@ -1020,7 +1020,7 @@
|
||||
},
|
||||
{
|
||||
"header":"Платежив 2027 году",
|
||||
"accessorKey":"data.contract_summary.future_payments_y1",
|
||||
"accessorKey":"data.contract_summary.future_y1",
|
||||
"columnLetter":"Q",
|
||||
"size":190,
|
||||
"filterFn":"contains",
|
||||
@ -1033,7 +1033,7 @@
|
||||
},
|
||||
{
|
||||
"header":"Платежив 2028 году",
|
||||
"accessorKey":"data.contract_summary.future_payments_y2",
|
||||
"accessorKey":"data.contract_summary.future_y2",
|
||||
"columnLetter":"R",
|
||||
"size":190,
|
||||
"filterFn":"contains",
|
||||
@ -1046,7 +1046,7 @@
|
||||
},
|
||||
{
|
||||
"header":"Сумма в других ССП",
|
||||
"accessorKey":"data.contract_summary.other_ssp_amount",
|
||||
"accessorKey":"data.contract_summary.other_ssp",
|
||||
"columnLetter":"S",
|
||||
"size":180,
|
||||
"filterFn":"contains",
|
||||
@ -1475,7 +1475,7 @@
|
||||
"columns":[
|
||||
{
|
||||
"header":"Сумма (без НДС)",
|
||||
"accessorKey":"data.collegial.amount",
|
||||
"accessorKey":"data.collegial.approved",
|
||||
"columnLetter":"AS",
|
||||
"size":150,
|
||||
"filterFn":"contains",
|
||||
@ -1488,7 +1488,7 @@
|
||||
},
|
||||
{
|
||||
"header":"Реквизиты протокола (дата, номер)",
|
||||
"accessorKey":"data.collegial.protocol_reference",
|
||||
"accessorKey":"data.collegial.protocol",
|
||||
"columnLetter":"AT",
|
||||
"size":330,
|
||||
"filterFn":"contains",
|
||||
@ -1541,7 +1541,7 @@
|
||||
},
|
||||
{
|
||||
"header":"Расходы 1 кв. 2026 г. тыс. руб. (без НДС)",
|
||||
"accessorKey":"data.ckk.expenses_q1",
|
||||
"accessorKey":"data.ckk.q1",
|
||||
"columnLetter":"AY",
|
||||
"size":420,
|
||||
"filterFn":"contains",
|
||||
@ -1554,7 +1554,7 @@
|
||||
},
|
||||
{
|
||||
"header":"Расходы 2 кв. 2026 г. тыс. руб. (без НДС)",
|
||||
"accessorKey":"data.ckk.expenses_q2",
|
||||
"accessorKey":"data.ckk.q2",
|
||||
"columnLetter":"AZ",
|
||||
"size":420,
|
||||
"filterFn":"contains",
|
||||
@ -1567,7 +1567,7 @@
|
||||
},
|
||||
{
|
||||
"header":"Расходы 3 кв. 2026 г. тыс. руб. (без НДС)",
|
||||
"accessorKey":"data.ckk.expenses_q3",
|
||||
"accessorKey":"data.ckk.q3",
|
||||
"columnLetter":"BA",
|
||||
"size":420,
|
||||
"filterFn":"contains",
|
||||
@ -1580,7 +1580,7 @@
|
||||
},
|
||||
{
|
||||
"header":"Расходы 4 кв. 2026 г. тыс. руб. (без НДС)",
|
||||
"accessorKey":"data.ckk.expenses_q4",
|
||||
"accessorKey":"data.ckk.q4",
|
||||
"columnLetter":"BB",
|
||||
"size":420,
|
||||
"filterFn":"contains",
|
||||
@ -1606,7 +1606,7 @@
|
||||
},
|
||||
{
|
||||
"header":"Срок поставки",
|
||||
"accessorKey":"data.ckk.delivery_deadline",
|
||||
"accessorKey":"data.ckk.deadline",
|
||||
"columnLetter":"BD",
|
||||
"size":150,
|
||||
"filterFn":"contains",
|
||||
@ -1619,7 +1619,7 @@
|
||||
},
|
||||
{
|
||||
"header":"План закупок",
|
||||
"accessorKey":"data.ckk.procurement_plan",
|
||||
"accessorKey":"data.ckk.proc_plan",
|
||||
"columnLetter":"BE",
|
||||
"size":150,
|
||||
"filterFn":"contains",
|
||||
@ -1632,7 +1632,7 @@
|
||||
},
|
||||
{
|
||||
"header":"Способ закупки",
|
||||
"accessorKey":"data.ckk.procurement_method",
|
||||
"accessorKey":"data.ckk.proc_method",
|
||||
"columnLetter":"BF",
|
||||
"size":150,
|
||||
"filterFn":"contains",
|
||||
@ -1748,7 +1748,7 @@
|
||||
},
|
||||
{
|
||||
"header":"Расходы 1 кв. 2026 г. тыс. руб. (без НДС)",
|
||||
"accessorKey":"data.contract.expenses_q1",
|
||||
"accessorKey":"data.contract.q1",
|
||||
"columnLetter":"BO",
|
||||
"size":420,
|
||||
"filterFn":"contains",
|
||||
@ -1761,7 +1761,7 @@
|
||||
},
|
||||
{
|
||||
"header":"Расходы 2 кв. 2026 г. тыс. руб. (без НДС)",
|
||||
"accessorKey":"data.contract.expenses_q2",
|
||||
"accessorKey":"data.contract.q2",
|
||||
"columnLetter":"BP",
|
||||
"size":420,
|
||||
"filterFn":"contains",
|
||||
@ -1774,7 +1774,7 @@
|
||||
},
|
||||
{
|
||||
"header":"Расходы 3 кв. 2026 г. тыс. руб. (без НДС)",
|
||||
"accessorKey":"data.contract.expenses_q3",
|
||||
"accessorKey":"data.contract.q3",
|
||||
"columnLetter":"BQ",
|
||||
"size":420,
|
||||
"filterFn":"contains",
|
||||
@ -1787,7 +1787,7 @@
|
||||
},
|
||||
{
|
||||
"header":"Расходы 4 кв. 2026 г. тыс. руб. (без НДС)",
|
||||
"accessorKey":"data.contract.expenses_q4",
|
||||
"accessorKey":"data.contract.q4",
|
||||
"columnLetter":"BR",
|
||||
"size":420,
|
||||
"filterFn":"contains",
|
||||
|
||||
@ -45,7 +45,7 @@ function DictVspPage() {
|
||||
const isAdmin = user.role_id == ROLES_NAME_ID.admin;
|
||||
const [vspResponse, sspResponse] = await Promise.all([
|
||||
DictVspApi.get(),
|
||||
isAdmin ? SspApi.getAll() : Promise.resolve(null),
|
||||
isAdmin ? SspApi.getAll({is_active: true}) : Promise.resolve(null),
|
||||
]);
|
||||
|
||||
if (!vspResponse.success) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user