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,
|
BaseSingleResponse,
|
||||||
CellPatch,
|
CellPatch,
|
||||||
CellsPatch,
|
CellsPatch,
|
||||||
|
ResponseBase,
|
||||||
SheetResponse,
|
SheetResponse,
|
||||||
UpdProjectBody,
|
UpdProjectBody,
|
||||||
)
|
)
|
||||||
@ -251,6 +252,17 @@ async def upd_project(
|
|||||||
return result
|
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")
|
@router.post("/projects")
|
||||||
async def add_project(
|
async def add_project(
|
||||||
body: AddProjectBody,
|
body: AddProjectBody,
|
||||||
|
|||||||
@ -232,12 +232,14 @@ class ConnectionManager:
|
|||||||
form_key: dict,
|
form_key: dict,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not cell_key:
|
if not cell_key:
|
||||||
|
print("not cell key")
|
||||||
return False
|
return False
|
||||||
cell_key = frozenset(cell_key.items())
|
cell_key = frozenset(cell_key.items())
|
||||||
|
|
||||||
lock_key = (con_key, frozenset(form_key.items()), cell_key)
|
lock_key = (con_key, frozenset(form_key.items()), cell_key)
|
||||||
lock_owner_id = self.cell_locks.get(lock_key)
|
lock_owner_id = self.cell_locks.get(lock_key)
|
||||||
if lock_owner_id is not None and lock_owner_id != user_id:
|
if lock_owner_id is not None and lock_owner_id != user_id:
|
||||||
|
print(lock_owner_id, user_id)
|
||||||
return False
|
return False
|
||||||
self.cell_locks[lock_key] = user_id
|
self.cell_locks[lock_key] = user_id
|
||||||
return True
|
return True
|
||||||
@ -283,7 +285,11 @@ class ConnectionManager:
|
|||||||
if not cell_key:
|
if not cell_key:
|
||||||
return True
|
return True
|
||||||
cell_key = frozenset(cell_key.items())
|
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))
|
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
|
return lock_owner_id is not None and lock_owner_id == user_id
|
||||||
|
|
||||||
def is_row_locked_by_other(
|
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:
|
def resolve_cell_key(event_data: dict) -> dict | None:
|
||||||
data = event_data.get("data")
|
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 None
|
||||||
return {
|
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"],
|
"column": data["column"],
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -751,6 +757,7 @@ async def process_websocket(
|
|||||||
try:
|
try:
|
||||||
event_name = data.get("event")
|
event_name = data.get("event")
|
||||||
cell_key = resolve_cell_key(data)
|
cell_key = resolve_cell_key(data)
|
||||||
|
print(cell_key, "da")
|
||||||
|
|
||||||
match event_name:
|
match event_name:
|
||||||
case "cell_edit_start":
|
case "cell_edit_start":
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
from __future__ import annotations
|
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 sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from src.db.base import Base
|
from src.db.base import Base
|
||||||
@ -41,6 +43,7 @@ class Project(Base):
|
|||||||
staff_count: Mapped[int | None] = mapped_column(Integer)
|
staff_count: Mapped[int | None] = mapped_column(Integer)
|
||||||
total_area: Mapped[float | None] = mapped_column(Numeric)
|
total_area: Mapped[float | None] = mapped_column(Numeric)
|
||||||
org_unit_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("v3.org_unit.id"))
|
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")
|
# parent = relationship("Project", remote_side="Project.id", back_populates="children")
|
||||||
# children: Mapped[list["Project"]] = relationship("Project", back_populates="parent") # type: ignore
|
# 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,
|
"total_area": float(project.total_area) if project.total_area is not None else None,
|
||||||
"org_unit_id": project.org_unit_id,
|
"org_unit_id": project.org_unit_id,
|
||||||
"org_unit_name": org_unit_name,
|
"org_unit_name": org_unit_name,
|
||||||
|
"created_at": project.created_at.isoformat() if project.created_at else None,
|
||||||
"report_count": int(report_count),
|
"report_count": int(report_count),
|
||||||
"years": years,
|
"years": years,
|
||||||
}
|
}
|
||||||
@ -316,6 +317,15 @@ class ProjectRepository:
|
|||||||
rows = (await self.db.execute(query, {"line_id": line_id})).all()
|
rows = (await self.db.execute(query, {"line_id": line_id})).all()
|
||||||
return [tuple(r) for r in rows]
|
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):
|
async def upd_project(self, project_id: int, column: str, value):
|
||||||
query = text(
|
query = text(
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -147,6 +147,12 @@ class ProjectService:
|
|||||||
raise ValidationException("Отчёт не найден")
|
raise ValidationException("Отчёт не найден")
|
||||||
return await self.project_repo.del_form3_line(line_id)
|
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(
|
async def upd_project(
|
||||||
self,
|
self,
|
||||||
project_id: int,
|
project_id: int,
|
||||||
|
|||||||
@ -85,6 +85,11 @@ class UserService:
|
|||||||
)
|
)
|
||||||
update_payload["role_id"] = role.value
|
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:
|
if "username" in update_payload and update_payload["username"] != target.username:
|
||||||
conflict = await self.user_repo.get_by_username(update_payload["username"])
|
conflict = await self.user_repo.get_by_username(update_payload["username"])
|
||||||
if conflict:
|
if conflict:
|
||||||
@ -122,6 +127,10 @@ class UserService:
|
|||||||
async def delete_user(self, user_id: int, user: AppUser):
|
async def delete_user(self, user_id: int, user: AppUser):
|
||||||
if user.role_id != UserRoleEnum.ADMIN:
|
if user.role_id != UserRoleEnum.ADMIN:
|
||||||
raise AccessDeniedException()
|
raise AccessDeniedException()
|
||||||
|
if user.id == user_id:
|
||||||
|
raise ValidationException(
|
||||||
|
"Нельзя удалить себя", field="role_id"
|
||||||
|
)
|
||||||
deleted = await self.user_repo.logical_delete(user_id)
|
deleted = await self.user_repo.logical_delete(user_id)
|
||||||
if not deleted:
|
if not deleted:
|
||||||
raise UserNotFoundException()
|
raise UserNotFoundException()
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
from datetime import datetime
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -12,11 +13,11 @@ def test_projects_list_smoke(client, admin_tokens, auth_headers):
|
|||||||
assert isinstance(payload["result"], list)
|
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]}"
|
name = f"PT_{uuid.uuid4().hex[:10]}"
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/projects",
|
"/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),
|
headers=auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
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
|
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):
|
for expense_item_id in range(1, 80):
|
||||||
response = client.post(
|
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},
|
json={"expense_item_id": expense_item_id},
|
||||||
headers=auth_headers(admin_tokens),
|
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()
|
payload = response.json()
|
||||||
assert isinstance(payload, dict)
|
assert isinstance(payload, dict)
|
||||||
assert payload.get("message") == "Отчёт не найден"
|
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,
|
"color_type":greenColumn,
|
||||||
"accessorKey":"data.plan.comment"
|
"accessorKey":"data.plan.comment"
|
||||||
},
|
},
|
||||||
"data.contract_summary.amount":{
|
"data.contract_summary.total":{
|
||||||
"color_type":greenColumn,
|
"color_type":greenColumn,
|
||||||
"accessorKey":"data.contract_summary.amount"
|
"accessorKey":"data.contract_summary.total"
|
||||||
},
|
},
|
||||||
"data.contract_summary.reference":{
|
"data.contract_summary.reference":{
|
||||||
"color_type":greenColumn,
|
"color_type":greenColumn,
|
||||||
@ -66,17 +66,17 @@
|
|||||||
"color_type":greenColumn,
|
"color_type":greenColumn,
|
||||||
"accessorKey":"data.contract_summary.comment"
|
"accessorKey":"data.contract_summary.comment"
|
||||||
},
|
},
|
||||||
"data.contract_summary.future_payments_y1":{
|
"data.contract_summary.future_y1":{
|
||||||
"color_type":greenColumn,
|
"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,
|
"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,
|
"color_type":greenColumn,
|
||||||
"accessorKey":"data.contract_summary.other_ssp_amount"
|
"accessorKey":"data.contract_summary.other_ssp"
|
||||||
},
|
},
|
||||||
"data.contract_summary.centralized_flag":{
|
"data.contract_summary.centralized_flag":{
|
||||||
"color_type":greenColumn,
|
"color_type":greenColumn,
|
||||||
@ -158,13 +158,13 @@
|
|||||||
"color_type":greenColumn,
|
"color_type":greenColumn,
|
||||||
"accessorKey":"data.approved.year"
|
"accessorKey":"data.approved.year"
|
||||||
},
|
},
|
||||||
"data.collegial.amount":{
|
"data.collegial.approved":{
|
||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
"accessorKey":"data.collegial.amount"
|
"accessorKey":"data.collegial.approved"
|
||||||
},
|
},
|
||||||
"data.collegial.protocol_reference":{
|
"data.collegial.protocol":{
|
||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
"accessorKey":"data.collegial.protocol_reference"
|
"accessorKey":"data.collegial.protocol"
|
||||||
},
|
},
|
||||||
"field_kollegialnye_organy_field":{
|
"field_kollegialnye_organy_field":{
|
||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
@ -182,37 +182,37 @@
|
|||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
"accessorKey":"data.ckk.ceiling"
|
"accessorKey":"data.ckk.ceiling"
|
||||||
},
|
},
|
||||||
"data.ckk.expenses_q1":{
|
"data.ckk.q1":{
|
||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
"accessorKey":"data.ckk.expenses_q1"
|
"accessorKey":"data.ckk.q1"
|
||||||
},
|
},
|
||||||
"data.ckk.expenses_q2":{
|
"data.ckk.q2":{
|
||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
"accessorKey":"data.ckk.expenses_q2"
|
"accessorKey":"data.ckk.q2"
|
||||||
},
|
},
|
||||||
"data.ckk.expenses_q3":{
|
"data.ckk.q3":{
|
||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
"accessorKey":"data.ckk.expenses_q3"
|
"accessorKey":"data.ckk.q3"
|
||||||
},
|
},
|
||||||
"data.ckk.expenses_q4":{
|
"data.ckk.q4":{
|
||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
"accessorKey":"data.ckk.expenses_q4"
|
"accessorKey":"data.ckk.q4"
|
||||||
},
|
},
|
||||||
"data.ckk.rf_schedule":{
|
"data.ckk.rf_schedule":{
|
||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
"accessorKey":"data.ckk.rf_schedule"
|
"accessorKey":"data.ckk.rf_schedule"
|
||||||
},
|
},
|
||||||
"data.ckk.delivery_deadline":{
|
"data.ckk.deadline":{
|
||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
"accessorKey":"data.ckk.delivery_deadline"
|
"accessorKey":"data.ckk.deadline"
|
||||||
},
|
},
|
||||||
"data.ckk.procurement_plan":{
|
"data.ckk.proc_plan":{
|
||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
"accessorKey":"data.ckk.procurement_plan"
|
"accessorKey":"data.ckk.proc_plan"
|
||||||
},
|
},
|
||||||
"data.ckk.procurement_method":{
|
"data.ckk.proc_method":{
|
||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
"accessorKey":"data.ckk.procurement_method"
|
"accessorKey":"data.ckk.proc_method"
|
||||||
},
|
},
|
||||||
"data.ckk.comment":{
|
"data.ckk.comment":{
|
||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
@ -242,21 +242,21 @@
|
|||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
"accessorKey":"data.contract.ceiling"
|
"accessorKey":"data.contract.ceiling"
|
||||||
},
|
},
|
||||||
"data.contract.expenses_q1":{
|
"data.contract.q1":{
|
||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
"accessorKey":"data.contract.expenses_q1"
|
"accessorKey":"data.contract.q1"
|
||||||
},
|
},
|
||||||
"data.contract.expenses_q2":{
|
"data.contract.q2":{
|
||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
"accessorKey":"data.contract.expenses_q2"
|
"accessorKey":"data.contract.q2"
|
||||||
},
|
},
|
||||||
"data.contract.expenses_q3":{
|
"data.contract.q3":{
|
||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
"accessorKey":"data.contract.expenses_q3"
|
"accessorKey":"data.contract.q3"
|
||||||
},
|
},
|
||||||
"data.contract.expenses_q4":{
|
"data.contract.q4":{
|
||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
"accessorKey":"data.contract.expenses_q4"
|
"accessorKey":"data.contract.q4"
|
||||||
},
|
},
|
||||||
"data.contract.rf_schedule":{
|
"data.contract.rf_schedule":{
|
||||||
"color_type":orangeColumn,
|
"color_type":orangeColumn,
|
||||||
@ -955,7 +955,7 @@
|
|||||||
"columns":[
|
"columns":[
|
||||||
{
|
{
|
||||||
"header":"Сумма",
|
"header":"Сумма",
|
||||||
"accessorKey":"data.contract_summary.amount",
|
"accessorKey":"data.contract_summary.total",
|
||||||
"columnLetter":"L",
|
"columnLetter":"L",
|
||||||
"size":150,
|
"size":150,
|
||||||
"filterFn":"contains",
|
"filterFn":"contains",
|
||||||
@ -1020,7 +1020,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"header":"Платежив 2027 году",
|
"header":"Платежив 2027 году",
|
||||||
"accessorKey":"data.contract_summary.future_payments_y1",
|
"accessorKey":"data.contract_summary.future_y1",
|
||||||
"columnLetter":"Q",
|
"columnLetter":"Q",
|
||||||
"size":190,
|
"size":190,
|
||||||
"filterFn":"contains",
|
"filterFn":"contains",
|
||||||
@ -1033,7 +1033,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"header":"Платежив 2028 году",
|
"header":"Платежив 2028 году",
|
||||||
"accessorKey":"data.contract_summary.future_payments_y2",
|
"accessorKey":"data.contract_summary.future_y2",
|
||||||
"columnLetter":"R",
|
"columnLetter":"R",
|
||||||
"size":190,
|
"size":190,
|
||||||
"filterFn":"contains",
|
"filterFn":"contains",
|
||||||
@ -1046,7 +1046,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"header":"Сумма в других ССП",
|
"header":"Сумма в других ССП",
|
||||||
"accessorKey":"data.contract_summary.other_ssp_amount",
|
"accessorKey":"data.contract_summary.other_ssp",
|
||||||
"columnLetter":"S",
|
"columnLetter":"S",
|
||||||
"size":180,
|
"size":180,
|
||||||
"filterFn":"contains",
|
"filterFn":"contains",
|
||||||
@ -1475,7 +1475,7 @@
|
|||||||
"columns":[
|
"columns":[
|
||||||
{
|
{
|
||||||
"header":"Сумма (без НДС)",
|
"header":"Сумма (без НДС)",
|
||||||
"accessorKey":"data.collegial.amount",
|
"accessorKey":"data.collegial.approved",
|
||||||
"columnLetter":"AS",
|
"columnLetter":"AS",
|
||||||
"size":150,
|
"size":150,
|
||||||
"filterFn":"contains",
|
"filterFn":"contains",
|
||||||
@ -1488,7 +1488,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"header":"Реквизиты протокола (дата, номер)",
|
"header":"Реквизиты протокола (дата, номер)",
|
||||||
"accessorKey":"data.collegial.protocol_reference",
|
"accessorKey":"data.collegial.protocol",
|
||||||
"columnLetter":"AT",
|
"columnLetter":"AT",
|
||||||
"size":330,
|
"size":330,
|
||||||
"filterFn":"contains",
|
"filterFn":"contains",
|
||||||
@ -1541,7 +1541,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"header":"Расходы 1 кв. 2026 г. тыс. руб. (без НДС)",
|
"header":"Расходы 1 кв. 2026 г. тыс. руб. (без НДС)",
|
||||||
"accessorKey":"data.ckk.expenses_q1",
|
"accessorKey":"data.ckk.q1",
|
||||||
"columnLetter":"AY",
|
"columnLetter":"AY",
|
||||||
"size":420,
|
"size":420,
|
||||||
"filterFn":"contains",
|
"filterFn":"contains",
|
||||||
@ -1554,7 +1554,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"header":"Расходы 2 кв. 2026 г. тыс. руб. (без НДС)",
|
"header":"Расходы 2 кв. 2026 г. тыс. руб. (без НДС)",
|
||||||
"accessorKey":"data.ckk.expenses_q2",
|
"accessorKey":"data.ckk.q2",
|
||||||
"columnLetter":"AZ",
|
"columnLetter":"AZ",
|
||||||
"size":420,
|
"size":420,
|
||||||
"filterFn":"contains",
|
"filterFn":"contains",
|
||||||
@ -1567,7 +1567,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"header":"Расходы 3 кв. 2026 г. тыс. руб. (без НДС)",
|
"header":"Расходы 3 кв. 2026 г. тыс. руб. (без НДС)",
|
||||||
"accessorKey":"data.ckk.expenses_q3",
|
"accessorKey":"data.ckk.q3",
|
||||||
"columnLetter":"BA",
|
"columnLetter":"BA",
|
||||||
"size":420,
|
"size":420,
|
||||||
"filterFn":"contains",
|
"filterFn":"contains",
|
||||||
@ -1580,7 +1580,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"header":"Расходы 4 кв. 2026 г. тыс. руб. (без НДС)",
|
"header":"Расходы 4 кв. 2026 г. тыс. руб. (без НДС)",
|
||||||
"accessorKey":"data.ckk.expenses_q4",
|
"accessorKey":"data.ckk.q4",
|
||||||
"columnLetter":"BB",
|
"columnLetter":"BB",
|
||||||
"size":420,
|
"size":420,
|
||||||
"filterFn":"contains",
|
"filterFn":"contains",
|
||||||
@ -1606,7 +1606,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"header":"Срок поставки",
|
"header":"Срок поставки",
|
||||||
"accessorKey":"data.ckk.delivery_deadline",
|
"accessorKey":"data.ckk.deadline",
|
||||||
"columnLetter":"BD",
|
"columnLetter":"BD",
|
||||||
"size":150,
|
"size":150,
|
||||||
"filterFn":"contains",
|
"filterFn":"contains",
|
||||||
@ -1619,7 +1619,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"header":"План закупок",
|
"header":"План закупок",
|
||||||
"accessorKey":"data.ckk.procurement_plan",
|
"accessorKey":"data.ckk.proc_plan",
|
||||||
"columnLetter":"BE",
|
"columnLetter":"BE",
|
||||||
"size":150,
|
"size":150,
|
||||||
"filterFn":"contains",
|
"filterFn":"contains",
|
||||||
@ -1632,7 +1632,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"header":"Способ закупки",
|
"header":"Способ закупки",
|
||||||
"accessorKey":"data.ckk.procurement_method",
|
"accessorKey":"data.ckk.proc_method",
|
||||||
"columnLetter":"BF",
|
"columnLetter":"BF",
|
||||||
"size":150,
|
"size":150,
|
||||||
"filterFn":"contains",
|
"filterFn":"contains",
|
||||||
@ -1748,7 +1748,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"header":"Расходы 1 кв. 2026 г. тыс. руб. (без НДС)",
|
"header":"Расходы 1 кв. 2026 г. тыс. руб. (без НДС)",
|
||||||
"accessorKey":"data.contract.expenses_q1",
|
"accessorKey":"data.contract.q1",
|
||||||
"columnLetter":"BO",
|
"columnLetter":"BO",
|
||||||
"size":420,
|
"size":420,
|
||||||
"filterFn":"contains",
|
"filterFn":"contains",
|
||||||
@ -1761,7 +1761,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"header":"Расходы 2 кв. 2026 г. тыс. руб. (без НДС)",
|
"header":"Расходы 2 кв. 2026 г. тыс. руб. (без НДС)",
|
||||||
"accessorKey":"data.contract.expenses_q2",
|
"accessorKey":"data.contract.q2",
|
||||||
"columnLetter":"BP",
|
"columnLetter":"BP",
|
||||||
"size":420,
|
"size":420,
|
||||||
"filterFn":"contains",
|
"filterFn":"contains",
|
||||||
@ -1774,7 +1774,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"header":"Расходы 3 кв. 2026 г. тыс. руб. (без НДС)",
|
"header":"Расходы 3 кв. 2026 г. тыс. руб. (без НДС)",
|
||||||
"accessorKey":"data.contract.expenses_q3",
|
"accessorKey":"data.contract.q3",
|
||||||
"columnLetter":"BQ",
|
"columnLetter":"BQ",
|
||||||
"size":420,
|
"size":420,
|
||||||
"filterFn":"contains",
|
"filterFn":"contains",
|
||||||
@ -1787,7 +1787,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"header":"Расходы 4 кв. 2026 г. тыс. руб. (без НДС)",
|
"header":"Расходы 4 кв. 2026 г. тыс. руб. (без НДС)",
|
||||||
"accessorKey":"data.contract.expenses_q4",
|
"accessorKey":"data.contract.q4",
|
||||||
"columnLetter":"BR",
|
"columnLetter":"BR",
|
||||||
"size":420,
|
"size":420,
|
||||||
"filterFn":"contains",
|
"filterFn":"contains",
|
||||||
|
|||||||
@ -45,7 +45,7 @@ function DictVspPage() {
|
|||||||
const isAdmin = user.role_id == ROLES_NAME_ID.admin;
|
const isAdmin = user.role_id == ROLES_NAME_ID.admin;
|
||||||
const [vspResponse, sspResponse] = await Promise.all([
|
const [vspResponse, sspResponse] = await Promise.all([
|
||||||
DictVspApi.get(),
|
DictVspApi.get(),
|
||||||
isAdmin ? SspApi.getAll() : Promise.resolve(null),
|
isAdmin ? SspApi.getAll({is_active: true}) : Promise.resolve(null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!vspResponse.success) {
|
if (!vspResponse.success) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user