Compare commits
10 Commits
3481e76b7c
...
b51b8dc0a5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b51b8dc0a5 | ||
|
|
004b956c77 | ||
| 3a93d193d3 | |||
| 6aa258d5e3 | |||
| 5d0b1aba9c | |||
| 9095cad992 | |||
| 8976a663a1 | |||
| 8a0c9540a0 | |||
| 351d3b9dd2 | |||
|
|
2586c1148d |
53
api/alembic/versions/0019_project_type.py
Normal file
53
api/alembic/versions/0019_project_type.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
revision = "0019"
|
||||||
|
down_revision = "0018"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Приводим перечень project_type к значениям из UI (projectConfig.jsx / ProjectTypeLiteral)
|
||||||
|
op.execute("ALTER TABLE v3.project DROP CONSTRAINT IF EXISTS project_project_type_check")
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
ALTER TABLE v3.project ADD CONSTRAINT project_project_type_check
|
||||||
|
CHECK (project_type IS NULL OR project_type IN (
|
||||||
|
'Открытие ВСП', 'Закрытие ВСП', 'Переезд ВСП', 'Реновация РФ', 'Открытие УРМ'
|
||||||
|
))
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обязательные поля проекта (как во фронтенде): заполняем NULL-ы дефолтами,
|
||||||
|
# затем накладываем NOT NULL
|
||||||
|
op.execute("UPDATE v3.project SET name = 'Проект' WHERE name IS NULL OR name = ''")
|
||||||
|
op.execute("UPDATE v3.project SET project_type = 'Открытие ВСП' WHERE project_type IS NULL")
|
||||||
|
op.execute("UPDATE v3.project SET vsp_format = 'Типовой' WHERE vsp_format IS NULL")
|
||||||
|
op.execute("UPDATE v3.project SET object_address = '' WHERE object_address IS NULL")
|
||||||
|
op.execute("UPDATE v3.project SET placement_type = 'own' WHERE placement_type IS NULL")
|
||||||
|
|
||||||
|
op.execute("ALTER TABLE v3.project ALTER COLUMN name SET NOT NULL")
|
||||||
|
op.execute("ALTER TABLE v3.project ALTER COLUMN project_type SET NOT NULL")
|
||||||
|
op.execute("ALTER TABLE v3.project ALTER COLUMN vsp_format SET NOT NULL")
|
||||||
|
op.execute("ALTER TABLE v3.project ALTER COLUMN object_address SET NOT NULL")
|
||||||
|
op.execute("ALTER TABLE v3.project ALTER COLUMN placement_type SET NOT NULL")
|
||||||
|
|
||||||
|
# Числовые значения проекта не могут быть отрицательными:
|
||||||
|
# сначала обнуляем уже существующие отрицательные значения,
|
||||||
|
# затем накладываем ограничения
|
||||||
|
op.execute("UPDATE v3.project SET staff_count = 0 WHERE staff_count < 0")
|
||||||
|
op.execute("UPDATE v3.project SET total_area = 0 WHERE total_area < 0")
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE v3.project ADD CONSTRAINT project_staff_count_check "
|
||||||
|
"CHECK (staff_count IS NULL OR staff_count >= 0)"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE v3.project ADD CONSTRAINT project_total_area_check "
|
||||||
|
"CHECK (total_area IS NULL OR total_area >= 0)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
@ -126,6 +126,27 @@ async def export_forms_bulk(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/project/{project_id}")
|
||||||
|
async def export_project(
|
||||||
|
project_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||||
|
):
|
||||||
|
export_service = ExportService(db)
|
||||||
|
try:
|
||||||
|
stream, filename, media_type = await export_service.export_project_payload(
|
||||||
|
project_id=project_id,
|
||||||
|
current_user=current_user,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
|
||||||
|
return StreamingResponse(
|
||||||
|
stream,
|
||||||
|
media_type=media_type,
|
||||||
|
headers={"Content-Disposition": export_service.build_content_disposition(filename)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/project/{project_id}/report/{year}/{report_type}")
|
@router.get("/project/{project_id}/report/{year}/{report_type}")
|
||||||
async def export_project_report(
|
async def export_project_report(
|
||||||
project_id: int,
|
project_id: int,
|
||||||
|
|||||||
@ -14,7 +14,6 @@ from src.db.session import get_db
|
|||||||
from src.domain.schemas import (
|
from src.domain.schemas import (
|
||||||
AddForm3LineBody,
|
AddForm3LineBody,
|
||||||
AddProjectBody,
|
AddProjectBody,
|
||||||
AddProjectYearBody,
|
|
||||||
BaseListResponse,
|
BaseListResponse,
|
||||||
BaseSingleResponse,
|
BaseSingleResponse,
|
||||||
CellPatch,
|
CellPatch,
|
||||||
@ -329,14 +328,12 @@ async def delete_project(
|
|||||||
@router.post("/projects/{project_id}/year")
|
@router.post("/projects/{project_id}/year")
|
||||||
async def add_project_year(
|
async def add_project_year(
|
||||||
project_id: int,
|
project_id: int,
|
||||||
body: AddProjectYearBody,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: AppUser = Depends(require_executor),
|
current_user: AppUser = Depends(require_executor),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
project_service = ProjectService(db)
|
project_service = ProjectService(db)
|
||||||
return await project_service.add_project_year(
|
return await project_service.add_project_year(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
year=body.year,
|
|
||||||
user=current_user,
|
user=current_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ class Project(Base):
|
|||||||
name="project_placement_type_check",
|
name="project_placement_type_check",
|
||||||
),
|
),
|
||||||
CheckConstraint(
|
CheckConstraint(
|
||||||
"project_type IS NULL OR project_type IN ('current', 'development')",
|
"project_type IS NULL OR project_type IN ('Открытие ВСП', 'Закрытие ВСП', 'Переезд ВСП', 'Реновация РФ', 'Открытие УРМ')",
|
||||||
name="project_project_type_check",
|
name="project_project_type_check",
|
||||||
),
|
),
|
||||||
CheckConstraint(
|
CheckConstraint(
|
||||||
@ -36,18 +36,26 @@ class Project(Base):
|
|||||||
"funding_by_ko_decision IS NULL OR funding_by_ko_decision IN ('prrs_budget', 'bank_reserve')",
|
"funding_by_ko_decision IS NULL OR funding_by_ko_decision IN ('prrs_budget', 'bank_reserve')",
|
||||||
name="project_funding_ko_check",
|
name="project_funding_ko_check",
|
||||||
),
|
),
|
||||||
|
CheckConstraint(
|
||||||
|
"staff_count IS NULL OR staff_count >= 0",
|
||||||
|
name="project_staff_count_check",
|
||||||
|
),
|
||||||
|
CheckConstraint(
|
||||||
|
"total_area IS NULL OR total_area >= 0",
|
||||||
|
name="project_total_area_check",
|
||||||
|
),
|
||||||
Index("ix_v3_project_ssp", "org_unit_id"),
|
Index("ix_v3_project_ssp", "org_unit_id"),
|
||||||
{"schema": "v3"},
|
{"schema": "v3"},
|
||||||
)
|
)
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
name: Mapped[str | None] = mapped_column(String)
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
level: Mapped[str | None] = mapped_column(String)
|
level: Mapped[str | None] = mapped_column(String)
|
||||||
parent_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("v3.project.id"))
|
parent_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("v3.project.id"))
|
||||||
project_type: Mapped[str | None] = mapped_column(String)
|
project_type: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
vsp_format: Mapped[str | None] = mapped_column(String)
|
vsp_format: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
placement_type: Mapped[str | None] = mapped_column(String)
|
placement_type: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
object_address: Mapped[str | None] = mapped_column(String)
|
object_address: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
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"))
|
||||||
|
|||||||
@ -388,8 +388,8 @@ class UpdProjectBody(BaseModel):
|
|||||||
vsp_format: Optional[VspFormatLiteral] = None
|
vsp_format: Optional[VspFormatLiteral] = None
|
||||||
placement_type: Optional[PlacementTypeLiteral] = None
|
placement_type: Optional[PlacementTypeLiteral] = None
|
||||||
object_address: Optional[str] = None
|
object_address: Optional[str] = None
|
||||||
staff_count: Optional[int] = None
|
staff_count: Optional[int] = Field(None, ge=0)
|
||||||
total_area: Optional[float] = None
|
total_area: Optional[float] = Field(None, ge=0)
|
||||||
org_unit_id: Optional[int] = None
|
org_unit_id: Optional[int] = None
|
||||||
krf_decision_date: Optional[date] = None
|
krf_decision_date: Optional[date] = None
|
||||||
fk_decision_date: Optional[date] = None
|
fk_decision_date: Optional[date] = None
|
||||||
@ -398,10 +398,6 @@ class UpdProjectBody(BaseModel):
|
|||||||
funding_by_ko_decision: Optional[Literal["prrs_budget", "bank_reserve"]] = None
|
funding_by_ko_decision: Optional[Literal["prrs_budget", "bank_reserve"]] = None
|
||||||
|
|
||||||
|
|
||||||
class AddProjectYearBody(BaseModel):
|
|
||||||
year: int
|
|
||||||
|
|
||||||
|
|
||||||
class UpdSmetaBody(BaseModel):
|
class UpdSmetaBody(BaseModel):
|
||||||
is_in_plan: Optional[bool] = None
|
is_in_plan: Optional[bool] = None
|
||||||
is_in_plan_q2: Optional[bool] = None
|
is_in_plan_q2: Optional[bool] = None
|
||||||
@ -423,12 +419,12 @@ class AddProjectBody(BaseModel):
|
|||||||
level: Literal["project", "program"] = "project"
|
level: Literal["project", "program"] = "project"
|
||||||
parent_id: Optional[int] = None
|
parent_id: Optional[int] = None
|
||||||
ssp_id: Optional[int] = None
|
ssp_id: Optional[int] = None
|
||||||
project_type: Optional[ProjectTypeLiteral] = None
|
project_type: ProjectTypeLiteral
|
||||||
vsp_format: Optional[VspFormatLiteral] = None
|
vsp_format: VspFormatLiteral
|
||||||
placement_type: Optional[PlacementTypeLiteral] = None
|
placement_type: PlacementTypeLiteral
|
||||||
object_address: Optional[str] = None
|
object_address: str = Field(..., min_length=1)
|
||||||
staff_count: Optional[int] = None
|
staff_count: Optional[int] = Field(None, ge=0)
|
||||||
total_area: Optional[float] = None
|
total_area: Optional[float] = Field(None, ge=0)
|
||||||
|
|
||||||
@field_validator("year", mode="before")
|
@field_validator("year", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@ -312,14 +312,28 @@ class ProjectRepository:
|
|||||||
query = query.where(Project.org_unit_id.in_(org_unit_ids))
|
query = query.where(Project.org_unit_id.in_(org_unit_ids))
|
||||||
return (await self.db.execute(query)).scalar_one_or_none()
|
return (await self.db.execute(query)).scalar_one_or_none()
|
||||||
|
|
||||||
async def get_report_rows(self, report_id: int, sections: list[str] | None = None) -> list[tuple]:
|
async def get_report_rows(
|
||||||
|
self,
|
||||||
|
report_id: int,
|
||||||
|
sections: list[str] | None = None,
|
||||||
|
user_id: int | None = None,
|
||||||
|
) -> list[tuple]:
|
||||||
query = text(
|
query = text(
|
||||||
"""
|
"""
|
||||||
SELECT row_type, depth, sort_order, data
|
SELECT row_type, depth, sort_order, data
|
||||||
FROM v3.v_form3_report_jsonb(CAST(:report_id AS INT), CAST(:sections AS TEXT[]))
|
FROM v3.v_form3_report_jsonb(
|
||||||
|
CAST(:report_id AS INT),
|
||||||
|
CAST(:sections AS TEXT[]),
|
||||||
|
CAST(:user_id AS INT)
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
return (await self.db.execute(query, {"report_id": report_id, "sections": sections})).all()
|
return (
|
||||||
|
await self.db.execute(
|
||||||
|
query,
|
||||||
|
{"report_id": report_id, "sections": sections, "user_id": user_id},
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
|
||||||
async def get_rf_rollup_rows(
|
async def get_rf_rollup_rows(
|
||||||
self,
|
self,
|
||||||
@ -480,6 +494,22 @@ class ProjectRepository:
|
|||||||
)
|
)
|
||||||
await self.db.flush()
|
await self.db.flush()
|
||||||
|
|
||||||
|
async def get_max_year(
|
||||||
|
self,
|
||||||
|
project_id: int,
|
||||||
|
org_unit_ids: list[int] | None = None,
|
||||||
|
) -> int | None:
|
||||||
|
query = (
|
||||||
|
select(func.max(RfProjectYear.year))
|
||||||
|
.join(Project, Project.id == RfProjectYear.project_id)
|
||||||
|
.where(Project.id == project_id)
|
||||||
|
)
|
||||||
|
if org_unit_ids is not None:
|
||||||
|
if len(org_unit_ids) == 0:
|
||||||
|
return None
|
||||||
|
query = query.where(Project.org_unit_id.in_(org_unit_ids))
|
||||||
|
return (await self.db.execute(query)).scalar_one_or_none()
|
||||||
|
|
||||||
async def resolve_rf_project_year_id(
|
async def resolve_rf_project_year_id(
|
||||||
self,
|
self,
|
||||||
project_id: int,
|
project_id: int,
|
||||||
|
|||||||
@ -1426,6 +1426,15 @@ PALETTE = {
|
|||||||
"INPUT": "FFFFFF",
|
"INPUT": "FFFFFF",
|
||||||
"COLOR": "000000",
|
"COLOR": "000000",
|
||||||
},
|
},
|
||||||
|
"project": {
|
||||||
|
"ROOT": "C2D59A",
|
||||||
|
"GROUP": "D7E3BC",
|
||||||
|
"ITEM": "EAF0DD",
|
||||||
|
"SUB_ITEM": "FFFFFF",
|
||||||
|
"SUB_ITEM": "EAF0DD",
|
||||||
|
"INPUT": "FFFFFF",
|
||||||
|
"COLOR": "000000",
|
||||||
|
},
|
||||||
"orange": {
|
"orange": {
|
||||||
"ROOT": "F1C297",
|
"ROOT": "F1C297",
|
||||||
"GROUP": "F6D6B9",
|
"GROUP": "F6D6B9",
|
||||||
@ -3132,13 +3141,389 @@ SMETA_COLUMNS: list[tuple[str, str]] = [
|
|||||||
("corrected_development_q3", "Корректировка развитие III квартал"),
|
("corrected_development_q3", "Корректировка развитие III квартал"),
|
||||||
("corrected_development_q4", "Корректировка развитие IV квартал"),
|
("corrected_development_q4", "Корректировка развитие IV квартал"),
|
||||||
]
|
]
|
||||||
FORM3_REPORT_COLUMNS: list[tuple[str, str]] = [
|
FORM3_LIMIT_DEPTH_COLUMNS: dict = {
|
||||||
("section", "Код раздела"),
|
"header": {
|
||||||
("item_id", "ID статьи"),
|
"name": "Данные",
|
||||||
("name", "Наименование"),
|
"children": {
|
||||||
("q1", "I квартал"),
|
"section_code": {"name": "Код раздела"},
|
||||||
("q2", "II квартал"),
|
"item_id": {"name": "ID Статьи"},
|
||||||
("q3", "III квартал"),
|
"num_group_id": {"name": "ID Группы номенклатуры"},
|
||||||
("q4", "IV квартал"),
|
"name": {"name": "Наименование"},
|
||||||
("totals", "ИТОГО"),
|
},
|
||||||
]
|
},
|
||||||
|
"q1": {
|
||||||
|
"name": "Отчет об исполнении лимита затрат, утверждённого Правлением Банка в I квартале",
|
||||||
|
"children": {
|
||||||
|
"korrektirovka_limita": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Корректировка лимита",
|
||||||
|
"children": {
|
||||||
|
"adj_by_items": {"name": "По статьям сметы*"},
|
||||||
|
"adj_increase": {"name": "Увеличение сметы**"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"itogo": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Итого",
|
||||||
|
"children": {"total_corr": {"name": ""}},
|
||||||
|
},
|
||||||
|
"fakt": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Факт",
|
||||||
|
"children": {
|
||||||
|
"m1": {"name": "За январь"},
|
||||||
|
"m2": {"name": "За февраль"},
|
||||||
|
"m3": {"name": "За март"},
|
||||||
|
"quarter_actual": {"name": "Итого I квартал"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ekonomiya": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Экономия (+), перерасход (-)",
|
||||||
|
"children": {"economy": {"name": ""}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"q2": {
|
||||||
|
"name": "Отчет об исполнении лимита затрат, утверждённого Правлением Банка в II квартале",
|
||||||
|
"children": {
|
||||||
|
"limit_ostatok": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Лимит затрат / остаток лимита затрат на II квартал",
|
||||||
|
"children": {"carryover": {"name": ""}},
|
||||||
|
},
|
||||||
|
"korrektirovka_limita": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Корректировка лимита",
|
||||||
|
"children": {
|
||||||
|
"adj_by_items": {"name": "По статьям сметы*"},
|
||||||
|
"adj_increase": {"name": "Увеличение сметы**"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"itogo": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Итого",
|
||||||
|
"children": {"total_corr": {"name": ""}},
|
||||||
|
},
|
||||||
|
"fakt": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Факт",
|
||||||
|
"children": {
|
||||||
|
"m1": {"name": "За апрель"},
|
||||||
|
"m2": {"name": "За май"},
|
||||||
|
"m3": {"name": "За июнь"},
|
||||||
|
"quarter_actual": {"name": "Итого II квартал"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ekonomiya": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Экономия (+), перерасход (-)",
|
||||||
|
"children": {"economy": {"name": ""}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"q3": {
|
||||||
|
"name": "Отчет об исполнении лимита затрат, утверждённого Правлением Банка в III квартале",
|
||||||
|
"children": {
|
||||||
|
"limit_ostatok": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Лимит затрат / остаток лимита затрат на III квартал",
|
||||||
|
"children": {"carryover": {"name": ""}},
|
||||||
|
},
|
||||||
|
"korrektirovka_limita": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Корректировка лимита",
|
||||||
|
"children": {
|
||||||
|
"adj_by_items": {"name": "По статьям сметы*"},
|
||||||
|
"adj_increase": {"name": "Увеличение сметы**"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"itogo": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Итого",
|
||||||
|
"children": {"total_corr": {"name": ""}},
|
||||||
|
},
|
||||||
|
"fakt": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Факт",
|
||||||
|
"children": {
|
||||||
|
"m1": {"name": "За июль"},
|
||||||
|
"m2": {"name": "За август"},
|
||||||
|
"m3": {"name": "За сентябрь"},
|
||||||
|
"quarter_actual": {"name": "Итого III квартал"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ekonomiya": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Экономия (+), перерасход (-)",
|
||||||
|
"children": {"economy": {"name": ""}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"q4": {
|
||||||
|
"name": "Отчет об исполнении лимита затрат, утверждённого Правлением Банка в IV квартале",
|
||||||
|
"children": {
|
||||||
|
"limit_ostatok": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Лимит затрат / остаток лимита затрат на IV квартал",
|
||||||
|
"children": {"carryover": {"name": ""}},
|
||||||
|
},
|
||||||
|
"korrektirovka_limita": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Корректировка лимита",
|
||||||
|
"children": {
|
||||||
|
"adj_by_items": {"name": "По статьям сметы*"},
|
||||||
|
"adj_increase": {"name": "Увеличение сметы**"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"itogo": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Итого",
|
||||||
|
"children": {"total_corr": {"name": ""}},
|
||||||
|
},
|
||||||
|
"fakt": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Факт",
|
||||||
|
"children": {
|
||||||
|
"m1": {"name": "За октябрь"},
|
||||||
|
"m2": {"name": "За ноябрь"},
|
||||||
|
"m3": {"name": "За декабрь"},
|
||||||
|
"spod": {"name": "СПОД"},
|
||||||
|
"quarter_actual": {"name": "Итого IV квартал"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ekonomiya": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Экономия (+), перерасход (-)",
|
||||||
|
"children": {"economy": {"name": ""}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
FORM3_CURRENT_EXPENSES_DEPTH_COLUMNS: dict = {
|
||||||
|
"header": {
|
||||||
|
"name": "Данные",
|
||||||
|
"children": {
|
||||||
|
"section_code": {"name": "Код раздела"},
|
||||||
|
"item_id": {"name": "ID Статьи"},
|
||||||
|
"num_group_id": {"name": "ID Группы номенклатуры"},
|
||||||
|
"name": {"name": "Наименование"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"q1": {
|
||||||
|
"name": "Отчет об исполнении текущих расходов в I квартале",
|
||||||
|
"children": {
|
||||||
|
"korrektirovka_limita": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Корректировка лимита",
|
||||||
|
"children": {
|
||||||
|
"adj_by_items": {"name": "По статьям сметы*"},
|
||||||
|
"adj_increase": {"name": "Увеличение сметы**"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"itogo": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Итого",
|
||||||
|
"children": {"total_corr": {"name": ""}},
|
||||||
|
},
|
||||||
|
"fakt": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Факт",
|
||||||
|
"children": {
|
||||||
|
"m1": {"name": "За январь"},
|
||||||
|
"m2": {"name": "За февраль"},
|
||||||
|
"m3": {"name": "За март"},
|
||||||
|
"quarter_actual": {"name": "Итого I квартал"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ekonomiya": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Экономия (+), перерасход (-)",
|
||||||
|
"children": {"economy": {"name": ""}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"q2": {
|
||||||
|
"name": "Отчет об исполнении текущих расходов в II квартале",
|
||||||
|
"children": {
|
||||||
|
"limit_ostatok": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Лимит затрат / остаток лимита затрат на II квартал",
|
||||||
|
"children": {"carryover": {"name": ""}},
|
||||||
|
},
|
||||||
|
"korrektirovka_limita": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Корректировка лимита",
|
||||||
|
"children": {
|
||||||
|
"adj_by_items": {"name": "По статьям сметы*"},
|
||||||
|
"adj_increase": {"name": "Увеличение сметы**"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"itogo": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Итого",
|
||||||
|
"children": {"total_corr": {"name": ""}},
|
||||||
|
},
|
||||||
|
"fakt": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Факт",
|
||||||
|
"children": {
|
||||||
|
"m1": {"name": "За апрель"},
|
||||||
|
"m2": {"name": "За май"},
|
||||||
|
"m3": {"name": "За июнь"},
|
||||||
|
"quarter_actual": {"name": "Итого II квартал"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ekonomiya": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Экономия (+), перерасход (-)",
|
||||||
|
"children": {"economy": {"name": ""}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"q3": {
|
||||||
|
"name": "Отчет об исполнении текущих расходов в III квартале",
|
||||||
|
"children": {
|
||||||
|
"limit_ostatok": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Лимит затрат / остаток лимита затрат на III квартал",
|
||||||
|
"children": {"carryover": {"name": ""}},
|
||||||
|
},
|
||||||
|
"korrektirovka_limita": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Корректировка лимита",
|
||||||
|
"children": {
|
||||||
|
"adj_by_items": {"name": "По статьям сметы*"},
|
||||||
|
"adj_increase": {"name": "Увеличение сметы**"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"itogo": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Итого",
|
||||||
|
"children": {"total_corr": {"name": ""}},
|
||||||
|
},
|
||||||
|
"fakt": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Факт",
|
||||||
|
"children": {
|
||||||
|
"m1": {"name": "За июль"},
|
||||||
|
"m2": {"name": "За август"},
|
||||||
|
"m3": {"name": "За сентябрь"},
|
||||||
|
"quarter_actual": {"name": "Итого III квартал"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ekonomiya": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Экономия (+), перерасход (-)",
|
||||||
|
"children": {"economy": {"name": ""}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"q4": {
|
||||||
|
"name": "Отчет об исполнении текущих расходов в IV квартале",
|
||||||
|
"children": {
|
||||||
|
"limit_ostatok": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Лимит затрат / остаток лимита затрат на IV квартал",
|
||||||
|
"children": {"carryover": {"name": ""}},
|
||||||
|
},
|
||||||
|
"korrektirovka_limita": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Корректировка лимита",
|
||||||
|
"children": {
|
||||||
|
"adj_by_items": {"name": "По статьям сметы*"},
|
||||||
|
"adj_increase": {"name": "Увеличение сметы**"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"itogo": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Итого",
|
||||||
|
"children": {"total_corr": {"name": ""}},
|
||||||
|
},
|
||||||
|
"fakt": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Факт",
|
||||||
|
"children": {
|
||||||
|
"m1": {"name": "За октябрь"},
|
||||||
|
"m2": {"name": "За ноябрь"},
|
||||||
|
"m3": {"name": "За декабрь"},
|
||||||
|
"spod": {"name": "СПОД"},
|
||||||
|
"quarter_actual": {"name": "Итого IV квартал"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ekonomiya": {
|
||||||
|
"key": None,
|
||||||
|
"name": "Экономия (+), перерасход (-)",
|
||||||
|
"children": {"economy": {"name": ""}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
FORM3_COLORS: dict = {
|
||||||
|
"header": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"header.section_code": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"header.item_id": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"header.num_group_id": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"header.name": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q1": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q1.korrektirovka_limita": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q1.korrektirovka_limita.adj_by_items": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q1.korrektirovka_limita.adj_increase": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q1.itogo": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q1.itogo.total_corr": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q1.fakt": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q1.fakt.m1": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q1.fakt.m2": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q1.fakt.m3": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q1.fakt.quarter_actual": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q1.ekonomiya": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q1.ekonomiya.economy": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q2": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q2.limit_ostatok": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q2.limit_ostatok.carryover": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q2.korrektirovka_limita": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q2.korrektirovka_limita.adj_by_items": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q2.korrektirovka_limita.adj_increase": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q2.itogo": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q2.itogo.total_corr": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q2.fakt": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q2.fakt.m1": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q2.fakt.m2": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q2.fakt.m3": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q2.fakt.quarter_actual": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q2.ekonomiya": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q2.ekonomiya.economy": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q3": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q3.limit_ostatok": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q3.limit_ostatok.carryover": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q3.korrektirovka_limita": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q3.korrektirovka_limita.adj_by_items": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q3.korrektirovka_limita.adj_increase": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q3.itogo": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q3.itogo.total_corr": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q3.fakt": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q3.fakt.m1": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q3.fakt.m2": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q3.fakt.m3": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q3.fakt.quarter_actual": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q3.ekonomiya": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q3.ekonomiya.economy": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q4": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q4.limit_ostatok": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q4.limit_ostatok.carryover": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q4.korrektirovka_limita": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q4.korrektirovka_limita.adj_by_items": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q4.korrektirovka_limita.adj_increase": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q4.itogo": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q4.itogo.total_corr": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q4.fakt": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q4.fakt.m1": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q4.fakt.m2": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q4.fakt.m3": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q4.fakt.spod": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q4.fakt.quarter_actual": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q4.ekonomiya": {"background": "C2D69B", "palette": "green"},
|
||||||
|
"q4.ekonomiya.economy": {"background": "C2D69B", "palette": "green"},
|
||||||
|
}
|
||||||
|
|||||||
@ -246,38 +246,25 @@ class SmetaRowNormalizer:
|
|||||||
return cur
|
return cur
|
||||||
|
|
||||||
|
|
||||||
class Form3ReportRowNormalizer:
|
class Form3DepthRowNormalizer:
|
||||||
|
def __init__(self, serializer: ExcelValueSerializer):
|
||||||
|
self.serializer = serializer
|
||||||
|
|
||||||
def normalize(self, data: dict[str, Any]) -> dict[str, Any]:
|
def normalize(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||||
normalized = dict(data)
|
normalized: dict[str, Any] = {}
|
||||||
header = data.get("header")
|
self._normalize_depth(normalized=normalized, original=data)
|
||||||
if isinstance(header, dict):
|
|
||||||
if "section_code" in header and "section" not in normalized:
|
|
||||||
normalized["section"] = header.get("section_code")
|
|
||||||
if "item_id" in header and "item_id" not in normalized:
|
|
||||||
normalized["item_id"] = header.get("item_id")
|
|
||||||
if "name" in header and "name" not in normalized:
|
|
||||||
normalized["name"] = header.get("name")
|
|
||||||
for quarter_key in ("q1", "q2", "q3", "q4"):
|
|
||||||
normalized[quarter_key] = self._extract_quarter_metric(normalized.get(quarter_key))
|
|
||||||
normalized["totals"] = self._extract_totals_metric(normalized.get("totals"))
|
|
||||||
normalized.pop("header", None)
|
|
||||||
normalized.pop("line_id", None)
|
|
||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
@staticmethod
|
def _normalize_depth(
|
||||||
def _extract_quarter_metric(value: Any) -> Any:
|
self,
|
||||||
if not isinstance(value, dict):
|
normalized: dict[str, Any],
|
||||||
return value
|
original: dict[str, Any],
|
||||||
for key in ("quarter_actual", "total_corr", "economy"):
|
prefix: str = "",
|
||||||
if value.get(key) is not None:
|
) -> None:
|
||||||
return value.get(key)
|
for key, value in original.items():
|
||||||
return None
|
if key == "line_id":
|
||||||
|
continue
|
||||||
@staticmethod
|
if isinstance(value, dict):
|
||||||
def _extract_totals_metric(value: Any) -> Any:
|
self._normalize_depth(normalized=normalized, original=value, prefix=f"{prefix}{key}.")
|
||||||
if not isinstance(value, dict):
|
else:
|
||||||
return value
|
normalized[f"{prefix}{key}"] = value
|
||||||
for key in ("total_actual", "total_corr", "economy"):
|
|
||||||
if value.get(key) is not None:
|
|
||||||
return value.get(key)
|
|
||||||
return None
|
|
||||||
|
|||||||
@ -18,14 +18,16 @@ from src.services.budget_form_service import BudgetFormService
|
|||||||
from src.services.export_service.export_normalizers import (
|
from src.services.export_service.export_normalizers import (
|
||||||
ExcelValueSerializer,
|
ExcelValueSerializer,
|
||||||
Form1DepthRowNormalizer,
|
Form1DepthRowNormalizer,
|
||||||
Form3ReportRowNormalizer,
|
Form3DepthRowNormalizer,
|
||||||
SmetaRowNormalizer,
|
SmetaRowNormalizer,
|
||||||
)
|
)
|
||||||
from src.services.export_service.export_mappings import (
|
from src.services.export_service.export_mappings import (
|
||||||
FORM1_COLORS,
|
FORM1_COLORS,
|
||||||
FORM1_DEPTH_COLUMNS,
|
FORM1_DEPTH_COLUMNS,
|
||||||
FORM2_DEPTH_AHR_SUB_COLUMNS,
|
FORM2_DEPTH_AHR_SUB_COLUMNS,
|
||||||
FORM3_REPORT_COLUMNS,
|
FORM3_COLORS,
|
||||||
|
FORM3_CURRENT_EXPENSES_DEPTH_COLUMNS,
|
||||||
|
FORM3_LIMIT_DEPTH_COLUMNS,
|
||||||
SMETA_COLUMNS,
|
SMETA_COLUMNS,
|
||||||
)
|
)
|
||||||
from src.services.export_service.export_writers import (
|
from src.services.export_service.export_writers import (
|
||||||
@ -50,6 +52,8 @@ SHEET_NAMES = {
|
|||||||
'AHR_UTILITY': 'АХР Коммунальные услуги',
|
'AHR_UTILITY': 'АХР Коммунальные услуги',
|
||||||
'SMETA': 'Смета',
|
'SMETA': 'Смета',
|
||||||
'STRUCTURE': 'Структура',
|
'STRUCTURE': 'Структура',
|
||||||
|
'LIMIT': 'Лимиты',
|
||||||
|
'CURRENT_EXPENSES': 'Текущие расходы',
|
||||||
}
|
}
|
||||||
SKIP_SHEETS = ('OTCH9F',)
|
SKIP_SHEETS = ('OTCH9F',)
|
||||||
|
|
||||||
@ -69,7 +73,7 @@ class ExportService:
|
|||||||
self.project_service = ProjectService(db)
|
self.project_service = ProjectService(db)
|
||||||
self.value_serializer = ExcelValueSerializer()
|
self.value_serializer = ExcelValueSerializer()
|
||||||
self.form1_row_normalizer = Form1DepthRowNormalizer(self.value_serializer)
|
self.form1_row_normalizer = Form1DepthRowNormalizer(self.value_serializer)
|
||||||
self.form3_report_row_normalizer = Form3ReportRowNormalizer()
|
self.form3_row_normalizer = Form3DepthRowNormalizer(self.value_serializer)
|
||||||
self.smeta_row_normalizer = SmetaRowNormalizer()
|
self.smeta_row_normalizer = SmetaRowNormalizer()
|
||||||
self.sheet_writers: dict[str, TabularSheetWriter] = {
|
self.sheet_writers: dict[str, TabularSheetWriter] = {
|
||||||
"__default__": DepthSheetWriter(
|
"__default__": DepthSheetWriter(
|
||||||
@ -122,11 +126,6 @@ class ExportService:
|
|||||||
colors_config=FORM1_COLORS["AHR"],
|
colors_config=FORM1_COLORS["AHR"],
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
self.form3_report_writer = TabularSheetWriter(
|
|
||||||
columns=FORM3_REPORT_COLUMNS,
|
|
||||||
row_normalizer=self.form3_report_row_normalizer.normalize,
|
|
||||||
value_serializer=self.value_serializer.to_excel_value,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def export_form_payload(
|
async def export_form_payload(
|
||||||
self,
|
self,
|
||||||
@ -164,6 +163,17 @@ class ExportService:
|
|||||||
)
|
)
|
||||||
return stream, filename, XLSX_MEDIA_TYPE
|
return stream, filename, XLSX_MEDIA_TYPE
|
||||||
|
|
||||||
|
async def export_project_payload(
|
||||||
|
self,
|
||||||
|
project_id: int,
|
||||||
|
current_user: AppUser,
|
||||||
|
) -> tuple[io.BytesIO, str, str]:
|
||||||
|
stream, filename = await self.export_project_to_xlsx(
|
||||||
|
project_id=project_id,
|
||||||
|
current_user=current_user,
|
||||||
|
)
|
||||||
|
return stream, filename, XLSX_MEDIA_TYPE
|
||||||
|
|
||||||
async def export_bulk_payload(
|
async def export_bulk_payload(
|
||||||
self,
|
self,
|
||||||
form_ids: list[int],
|
form_ids: list[int],
|
||||||
@ -300,14 +310,20 @@ class ExportService:
|
|||||||
user=current_user,
|
user=current_user,
|
||||||
)
|
)
|
||||||
report_type_upper = report_type.upper()
|
report_type_upper = report_type.upper()
|
||||||
|
project = await self.project_service.get(project_id, current_user)
|
||||||
workbook = Workbook()
|
workbook = Workbook()
|
||||||
meta_sheet = workbook.active
|
meta_sheet = workbook.active
|
||||||
meta_sheet.title = "metadata"
|
meta_sheet.title = "metadata"
|
||||||
metadata_rows = [
|
metadata_rows = [
|
||||||
("Тип формы", "FORM_3"),
|
("Тип формы", "FORM_3"),
|
||||||
("Project ID", project_id),
|
("Название проекта", project.get("name") if project else None),
|
||||||
("Year", year),
|
("Технический номер", project.get("technical_number") if project else None),
|
||||||
("Report Type", report_type_upper),
|
("Тип проекта", project.get("project_type") if project else None),
|
||||||
|
("Формат ВСП", project.get("vsp_format") if project else None),
|
||||||
|
("ССП/РФ", project.get("org_unit_name") if project else None),
|
||||||
|
("Статус", project.get("status") if project else None),
|
||||||
|
("Год", year),
|
||||||
|
("Тип отчёта", report_type_upper),
|
||||||
("Sections", ", ".join(sections) if sections else None),
|
("Sections", ", ".join(sections) if sections else None),
|
||||||
("Экспортировано", datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")),
|
("Экспортировано", datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")),
|
||||||
]
|
]
|
||||||
@ -315,7 +331,13 @@ class ExportService:
|
|||||||
meta_sheet.cell(row=row_idx, column=1, value=key)
|
meta_sheet.cell(row=row_idx, column=1, value=key)
|
||||||
meta_sheet.cell(row=row_idx, column=2, value=value)
|
meta_sheet.cell(row=row_idx, column=2, value=value)
|
||||||
worksheet = workbook.create_sheet(title=self._safe_sheet_name(report_type_upper))
|
worksheet = workbook.create_sheet(title=self._safe_sheet_name(report_type_upper))
|
||||||
self.form3_report_writer.write(worksheet, rows)
|
report_writer = DepthSheetWriter(
|
||||||
|
columns_dict=self._form3_columns(report_type_upper),
|
||||||
|
row_normalizer=self.form3_row_normalizer.normalize,
|
||||||
|
value_serializer=self.value_serializer.to_excel_value,
|
||||||
|
colors_config=FORM3_COLORS,
|
||||||
|
)
|
||||||
|
report_writer.write(worksheet, rows)
|
||||||
stream = io.BytesIO()
|
stream = io.BytesIO()
|
||||||
workbook.save(stream)
|
workbook.save(stream)
|
||||||
workbook.close()
|
workbook.close()
|
||||||
@ -324,11 +346,75 @@ class ExportService:
|
|||||||
filename = f"{year}_FORM_3_{project_id}_{report_type_upper}_{date_str}.xlsx"
|
filename = f"{year}_FORM_3_{project_id}_{report_type_upper}_{date_str}.xlsx"
|
||||||
return stream, filename
|
return stream, filename
|
||||||
|
|
||||||
|
async def export_project_to_xlsx(
|
||||||
|
self,
|
||||||
|
project_id: int,
|
||||||
|
current_user: AppUser,
|
||||||
|
) -> tuple[io.BytesIO, str]:
|
||||||
|
project = await self.project_service.get(project_id, current_user)
|
||||||
|
if not project:
|
||||||
|
raise ValueError(f"Проект {project_id} не найден")
|
||||||
|
|
||||||
|
reports = project.get("reports") or []
|
||||||
|
if not reports:
|
||||||
|
raise ValueError("У проекта нет отчётов для экспорта")
|
||||||
|
|
||||||
|
workbook = Workbook()
|
||||||
|
meta_sheet = workbook.active
|
||||||
|
meta_sheet.title = "metadata"
|
||||||
|
metadata_rows = [
|
||||||
|
("Тип формы", "FORM_3"),
|
||||||
|
("Название проекта", project.get("name")),
|
||||||
|
("Технический номер", project.get("technical_number")),
|
||||||
|
("Тип проекта", project.get("project_type")),
|
||||||
|
("Формат ВСП", project.get("vsp_format")),
|
||||||
|
("ССП/РФ", project.get("org_unit_name")),
|
||||||
|
("Статус", project.get("status")),
|
||||||
|
("Экспортировано", datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")),
|
||||||
|
]
|
||||||
|
for row_idx, (key, value) in enumerate(metadata_rows, start=1):
|
||||||
|
meta_sheet.cell(row=row_idx, column=1, value=key)
|
||||||
|
meta_sheet.cell(row=row_idx, column=2, value=value)
|
||||||
|
|
||||||
|
for report in reports:
|
||||||
|
report_type = str(report.get("report_type", "")).upper()
|
||||||
|
year = report.get("year")
|
||||||
|
rows = await self.project_service.get_report_rows(
|
||||||
|
project_id=project_id,
|
||||||
|
year=year,
|
||||||
|
report_type=report_type,
|
||||||
|
sections=None,
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
sheet_title = f"{year}_{self._safe_sheet_name(report_type)}"
|
||||||
|
worksheet = workbook.create_sheet(title=sheet_title)
|
||||||
|
report_writer = DepthSheetWriter(
|
||||||
|
columns_dict=self._form3_columns(report_type),
|
||||||
|
row_normalizer=self.form3_row_normalizer.normalize,
|
||||||
|
value_serializer=self.value_serializer.to_excel_value,
|
||||||
|
colors_config=FORM3_COLORS,
|
||||||
|
)
|
||||||
|
report_writer.write(worksheet, rows)
|
||||||
|
|
||||||
|
stream = io.BytesIO()
|
||||||
|
workbook.save(stream)
|
||||||
|
workbook.close()
|
||||||
|
stream.seek(0)
|
||||||
|
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||||||
|
filename = f"project_{project_id}_FORM_3_{date_str}.xlsx"
|
||||||
|
return stream, filename
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def build_content_disposition(filename: str) -> str:
|
def build_content_disposition(filename: str) -> str:
|
||||||
encoded = quote(filename)
|
encoded = quote(filename)
|
||||||
return f"attachment; filename*=UTF-8''{encoded}"
|
return f"attachment; filename*=UTF-8''{encoded}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _form3_columns(report_type: str) -> dict:
|
||||||
|
if (report_type or "").upper() == "LIMIT":
|
||||||
|
return FORM3_LIMIT_DEPTH_COLUMNS
|
||||||
|
return FORM3_CURRENT_EXPENSES_DEPTH_COLUMNS
|
||||||
|
|
||||||
def _fill_metadata(self, sheet: Worksheet, form: Any) -> None:
|
def _fill_metadata(self, sheet: Worksheet, form: Any) -> None:
|
||||||
metadata_rows = [
|
metadata_rows = [
|
||||||
("Форма ID", form.id),
|
("Форма ID", form.id),
|
||||||
|
|||||||
@ -101,7 +101,7 @@ class ProjectService:
|
|||||||
report_id = await self.resolve_report_id(project_id, year, report_type, user)
|
report_id = await self.resolve_report_id(project_id, year, report_type, user)
|
||||||
if not report_id:
|
if not report_id:
|
||||||
raise ValidationException("Отчёт не найден")
|
raise ValidationException("Отчёт не найден")
|
||||||
return await self.project_repo.get_report_rows(report_id=report_id, sections=sections)
|
return await self.project_repo.get_report_rows(report_id=report_id, sections=sections, user_id=user.id)
|
||||||
|
|
||||||
async def get_rf_rollup_rows(
|
async def get_rf_rollup_rows(
|
||||||
self,
|
self,
|
||||||
@ -224,7 +224,6 @@ class ProjectService:
|
|||||||
async def add_project_year(
|
async def add_project_year(
|
||||||
self,
|
self,
|
||||||
project_id: int,
|
project_id: int,
|
||||||
year: int,
|
|
||||||
user: AppUser,
|
user: AppUser,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
org_unit_ids = await self._allowed_org_unit_ids(user)
|
org_unit_ids = await self._allowed_org_unit_ids(user)
|
||||||
@ -234,14 +233,14 @@ class ProjectService:
|
|||||||
)
|
)
|
||||||
if not project:
|
if not project:
|
||||||
raise ValidationException("Проект не найден")
|
raise ValidationException("Проект не найден")
|
||||||
existing = await self.project_repo.resolve_rf_project_year_id(
|
last_year = await self.project_repo.get_max_year(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
year=year,
|
|
||||||
org_unit_ids=org_unit_ids,
|
org_unit_ids=org_unit_ids,
|
||||||
)
|
)
|
||||||
if existing is not None:
|
if not last_year:
|
||||||
raise ValidationException(f"Год {year} у проекта уже заведён")
|
raise ValidationException("У проекта нет года")
|
||||||
return await self.project_repo.add_project_year(project_id=project_id, year=year)
|
next_year = last_year + 1
|
||||||
|
return await self.project_repo.add_project_year(project_id=project_id, year=next_year)
|
||||||
|
|
||||||
async def add_project(
|
async def add_project(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@ -123,7 +123,15 @@ def test_backend_calls_form3_add_project_and_add_line_functions(
|
|||||||
):
|
):
|
||||||
create = client.post(
|
create = client.post(
|
||||||
"/api/v1/projects",
|
"/api/v1/projects",
|
||||||
json={"name": f"ITEST_DB_FUNC_{uuid.uuid4().hex[:6]}", "year": 2026, "branch_id": 1},
|
json={
|
||||||
|
"name": f"ITEST_DB_FUNC_{uuid.uuid4().hex[:6]}",
|
||||||
|
"year": 2026,
|
||||||
|
"branch_id": 1,
|
||||||
|
"project_type": "Открытие ВСП",
|
||||||
|
"vsp_format": "Типовой",
|
||||||
|
"placement_type": "Собственность",
|
||||||
|
"object_address": "Тестовая улица 1",
|
||||||
|
},
|
||||||
headers=auth_headers(admin_tokens),
|
headers=auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
assert create.status_code == 200
|
assert create.status_code == 200
|
||||||
@ -214,7 +222,15 @@ def test_forms_validation_direction_forbidden_on_non_form1(
|
|||||||
def test_form3_validation_invalid_sections_returns_400(client, admin_tokens, auth_headers):
|
def test_form3_validation_invalid_sections_returns_400(client, admin_tokens, auth_headers):
|
||||||
create = client.post(
|
create = client.post(
|
||||||
"/api/v1/projects",
|
"/api/v1/projects",
|
||||||
json={"name": f"ITEST_F3_SECT_{uuid.uuid4().hex[:6]}", "year": 2026, "branch_id": 1},
|
json={
|
||||||
|
"name": f"ITEST_F3_SECT_{uuid.uuid4().hex[:6]}",
|
||||||
|
"year": 2026,
|
||||||
|
"branch_id": 1,
|
||||||
|
"project_type": "Открытие ВСП",
|
||||||
|
"vsp_format": "Типовой",
|
||||||
|
"placement_type": "Собственность",
|
||||||
|
"object_address": "Тестовая улица 1",
|
||||||
|
},
|
||||||
headers=auth_headers(admin_tokens),
|
headers=auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
assert create.status_code == 200
|
assert create.status_code == 200
|
||||||
@ -232,7 +248,15 @@ def test_form3_validation_invalid_sections_returns_400(client, admin_tokens, aut
|
|||||||
def test_form3_validation_empty_sections_csv_is_ignored(client, admin_tokens, auth_headers):
|
def test_form3_validation_empty_sections_csv_is_ignored(client, admin_tokens, auth_headers):
|
||||||
create = client.post(
|
create = client.post(
|
||||||
"/api/v1/projects",
|
"/api/v1/projects",
|
||||||
json={"name": f"ITEST_F3_EMPTY_{uuid.uuid4().hex[:6]}", "year": 2026, "branch_id": 1},
|
json={
|
||||||
|
"name": f"ITEST_F3_EMPTY_{uuid.uuid4().hex[:6]}",
|
||||||
|
"year": 2026,
|
||||||
|
"branch_id": 1,
|
||||||
|
"project_type": "Открытие ВСП",
|
||||||
|
"vsp_format": "Типовой",
|
||||||
|
"placement_type": "Собственность",
|
||||||
|
"object_address": "Тестовая улица 1",
|
||||||
|
},
|
||||||
headers=auth_headers(admin_tokens),
|
headers=auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
assert create.status_code == 200
|
assert create.status_code == 200
|
||||||
@ -248,7 +272,15 @@ def test_form3_validation_empty_sections_csv_is_ignored(client, admin_tokens, au
|
|||||||
def test_form3_validation_duplicate_sections_is_allowed(client, admin_tokens, auth_headers):
|
def test_form3_validation_duplicate_sections_is_allowed(client, admin_tokens, auth_headers):
|
||||||
create = client.post(
|
create = client.post(
|
||||||
"/api/v1/projects",
|
"/api/v1/projects",
|
||||||
json={"name": f"ITEST_F3_DUP_{uuid.uuid4().hex[:6]}", "year": 2026, "branch_id": 1},
|
json={
|
||||||
|
"name": f"ITEST_F3_DUP_{uuid.uuid4().hex[:6]}",
|
||||||
|
"year": 2026,
|
||||||
|
"branch_id": 1,
|
||||||
|
"project_type": "Открытие ВСП",
|
||||||
|
"vsp_format": "Типовой",
|
||||||
|
"placement_type": "Собственность",
|
||||||
|
"object_address": "Тестовая улица 1",
|
||||||
|
},
|
||||||
headers=auth_headers(admin_tokens),
|
headers=auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
assert create.status_code == 200
|
assert create.status_code == 200
|
||||||
@ -265,7 +297,15 @@ def test_form3_add_project_invalid_name_returns_422(client, admin_tokens, auth_h
|
|||||||
# Пробел запрещён regex-правилом AddProjectBody.
|
# Пробел запрещён regex-правилом AddProjectBody.
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/projects",
|
"/api/v1/projects",
|
||||||
json={"name": "INVALID NAME", "year": 2026, "branch_id": 1},
|
json={
|
||||||
|
"name": "INVALID NAME",
|
||||||
|
"year": 2026,
|
||||||
|
"branch_id": 1,
|
||||||
|
"project_type": "Открытие ВСП",
|
||||||
|
"vsp_format": "Типовой",
|
||||||
|
"placement_type": "Собственность",
|
||||||
|
"object_address": "Тестовая улица 1",
|
||||||
|
},
|
||||||
headers=auth_headers(admin_tokens),
|
headers=auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
assert response.status_code == 422
|
assert response.status_code == 422
|
||||||
|
|||||||
@ -18,7 +18,15 @@ def _create_project(client, admin_tokens, auth_headers, year=2026, branch_id=1)
|
|||||||
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": year, "branch_id": branch_id},
|
json={
|
||||||
|
"name": name,
|
||||||
|
"year": year,
|
||||||
|
"branch_id": branch_id,
|
||||||
|
"project_type": "Открытие ВСП",
|
||||||
|
"vsp_format": "Типовой",
|
||||||
|
"placement_type": "Собственность",
|
||||||
|
"object_address": "Тестовая улица 1",
|
||||||
|
},
|
||||||
headers=auth_headers(admin_tokens),
|
headers=auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@ -54,7 +62,15 @@ def test_create_project_smoke(client, admin_tokens, auth_headers):
|
|||||||
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": None, "branch_id": 1},
|
json={
|
||||||
|
"name": name,
|
||||||
|
"year": None,
|
||||||
|
"branch_id": 1,
|
||||||
|
"project_type": "Открытие ВСП",
|
||||||
|
"vsp_format": "Типовой",
|
||||||
|
"placement_type": "Собственность",
|
||||||
|
"object_address": "Тестовая улица 1",
|
||||||
|
},
|
||||||
headers=auth_headers(admin_tokens),
|
headers=auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@ -361,7 +377,7 @@ def test_add_project_year_smoke(client, admin_tokens, auth_headers):
|
|||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
f"/api/v1/projects/{project_id}/year",
|
f"/api/v1/projects/{project_id}/year",
|
||||||
json={"year": 2027},
|
json={"year": 2050},
|
||||||
headers=auth_headers(admin_tokens),
|
headers=auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@ -377,7 +393,7 @@ def test_add_project_year_smoke(client, admin_tokens, auth_headers):
|
|||||||
assert 2027 in sub_years
|
assert 2027 in sub_years
|
||||||
|
|
||||||
|
|
||||||
def test_add_project_year_duplicate(client, admin_tokens, auth_headers):
|
def test_add_project_year_ignores_requested_year(client, admin_tokens, auth_headers):
|
||||||
project_id, _ = _create_project(client, admin_tokens, auth_headers, year=2026)
|
project_id, _ = _create_project(client, admin_tokens, auth_headers, year=2026)
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
@ -385,9 +401,9 @@ def test_add_project_year_duplicate(client, admin_tokens, auth_headers):
|
|||||||
json={"year": 2026},
|
json={"year": 2026},
|
||||||
headers=auth_headers(admin_tokens),
|
headers=auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
assert response.status_code == 400
|
assert response.status_code == 200
|
||||||
payload = response.json()
|
payload = response.json()
|
||||||
assert payload.get("message") == "Год 2026 у проекта уже заведён"
|
assert payload["year"] == 2027
|
||||||
|
|
||||||
|
|
||||||
def test_add_project_year_project_not_found(client, admin_tokens, auth_headers):
|
def test_add_project_year_project_not_found(client, admin_tokens, auth_headers):
|
||||||
|
|||||||
@ -35,7 +35,7 @@ INSERT INTO v3.vsp (id,branch_id,reg_number,address,format,opened_at,placement_t
|
|||||||
SELECT setval('v3.vsp_id_seq', 3);
|
SELECT setval('v3.vsp_id_seq', 3);
|
||||||
|
|
||||||
INSERT INTO v3.project ("id","name","level",parent_id,project_type,vsp_format,placement_type,object_address,staff_count,total_area,org_unit_id,status,technical_number,krf_decision_date,fk_decision_date,board_decision_date,open_relocate_close_date,funding_by_ko_decision) VALUES
|
INSERT INTO v3.project ("id","name","level",parent_id,project_type,vsp_format,placement_type,object_address,staff_count,total_area,org_unit_id,status,technical_number,krf_decision_date,fk_decision_date,board_decision_date,open_relocate_close_date,funding_by_ko_decision) VALUES
|
||||||
(2,'PT6d112310_U','project',NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,'created','2606_180801','2026-06-11','2026-06-18','2026-07-02','2026-07-15','prrs_budget');
|
(2,'PT6d112310_U','project',NULL,'Открытие ВСП','Типовой','own','Тестовая улица 1',NULL,NULL,1,'created','2606_180801','2026-06-11','2026-06-18','2026-07-02','2026-07-15','prrs_budget');
|
||||||
SELECT setval('v3.project_id_seq', 3);
|
SELECT setval('v3.project_id_seq', 3);
|
||||||
|
|
||||||
INSERT INTO v3.rf_project_year ("id",project_id,"year") VALUES
|
INSERT INTO v3.rf_project_year ("id",project_id,"year") VALUES
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from src.services.export_service.export_normalizers import (
|
from src.services.export_service.export_normalizers import (
|
||||||
ExcelValueSerializer,
|
ExcelValueSerializer,
|
||||||
Form1RowNormalizer,
|
Form1RowNormalizer,
|
||||||
Form3ReportRowNormalizer,
|
Form3DepthRowNormalizer,
|
||||||
SmetaRowNormalizer,
|
SmetaRowNormalizer,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -52,24 +52,29 @@ def test_excel_value_serializer_formats_collections():
|
|||||||
assert serializer.to_excel_value({"k": {"nested": 1}}) == "k=1"
|
assert serializer.to_excel_value({"k": {"nested": 1}}) == "k=1"
|
||||||
|
|
||||||
|
|
||||||
def test_form3_normalizer_flattens_quarters_and_totals_to_scalars():
|
def test_form3_depth_normalizer_flattens_nested_data():
|
||||||
normalizer = Form3ReportRowNormalizer()
|
normalizer = Form3DepthRowNormalizer(ExcelValueSerializer())
|
||||||
row = {
|
row = {
|
||||||
"header": {"section_code": "1.00.0.", "item_id": "R001", "name": "Строка"},
|
"line_id": 5,
|
||||||
"q1": {"quarter_actual": 10, "total_corr": 12},
|
"header": {"section_code": "1.00.0.", "item_id": "R001", "num_group_id": 7, "name": "Строка"},
|
||||||
"q2": {"total_corr": 22},
|
"q1": {"base_plan": 1, "adj_by_items": 2, "m1": 10, "quarter_actual": 20, "economy": 3},
|
||||||
"q3": {"economy": 1.5},
|
"q2": {"corrected_plan": 4, "carryover": 5},
|
||||||
"q4": {"m1": 100},
|
"totals": {"base_plan": 100, "total_actual": 40, "economy": 2.1},
|
||||||
"totals": {"total_actual": 40, "economy": 2.1},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
normalized = normalizer.normalize(row)
|
normalized = normalizer.normalize(row)
|
||||||
|
|
||||||
assert normalized["section"] == "1.00.0."
|
assert normalized["header.section_code"] == "1.00.0."
|
||||||
assert normalized["item_id"] == "R001"
|
assert normalized["header.item_id"] == "R001"
|
||||||
assert normalized["name"] == "Строка"
|
assert normalized["header.num_group_id"] == 7
|
||||||
assert normalized["q1"] == 10
|
assert normalized["header.name"] == "Строка"
|
||||||
assert normalized["q2"] == 22
|
assert normalized["q1.base_plan"] == 1
|
||||||
assert normalized["q3"] == 1.5
|
assert normalized["q1.adj_by_items"] == 2
|
||||||
assert normalized["q4"] is None
|
assert normalized["q1.m1"] == 10
|
||||||
assert normalized["totals"] == 40
|
assert normalized["q1.quarter_actual"] == 20
|
||||||
|
assert normalized["q2.corrected_plan"] == 4
|
||||||
|
assert normalized["q2.carryover"] == 5
|
||||||
|
assert normalized["totals.base_plan"] == 100
|
||||||
|
assert normalized["totals.total_actual"] == 40
|
||||||
|
assert normalized["totals.economy"] == 2.1
|
||||||
|
assert "line_id" not in normalized
|
||||||
|
|||||||
@ -43,4 +43,9 @@ export const ProjectsApi = {
|
|||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
exportProject: (id) => {
|
||||||
|
return api.get(`/export/project/${id}`, {
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -342,7 +342,7 @@ export const config = {
|
|||||||
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_i_kvartale_fakt',
|
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_i_kvartale_fakt',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'За ЯНВАРЬ',
|
header: 'За январь',
|
||||||
accessorKey: 'data.q1.m1',
|
accessorKey: 'data.q1.m1',
|
||||||
columnLetter: 'H',
|
columnLetter: 'H',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -355,7 +355,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'За ФЕВРАЛЬ',
|
header: 'За февраль',
|
||||||
accessorKey: 'data.q1.m2',
|
accessorKey: 'data.q1.m2',
|
||||||
columnLetter: 'I',
|
columnLetter: 'I',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -402,7 +402,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Экономия (+) перерасход (-)',
|
header: 'Экономия (+), перерасход (-)',
|
||||||
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_i_kvartale_ekonomiya_pereraskhod',
|
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_i_kvartale_ekonomiya_pereraskhod',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@ -440,7 +440,7 @@ export const config = {
|
|||||||
header: '',
|
header: '',
|
||||||
accessorKey: 'data.q2.carryover',
|
accessorKey: 'data.q2.carryover',
|
||||||
columnLetter: 'N',
|
columnLetter: 'N',
|
||||||
size: 150,
|
size: 250,
|
||||||
filterFn: 'contains',
|
filterFn: 'contains',
|
||||||
muiTableHeadCellProps: {
|
muiTableHeadCellProps: {
|
||||||
sx: {
|
sx: {
|
||||||
@ -525,7 +525,7 @@ export const config = {
|
|||||||
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_ii_kvartale_fakt',
|
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_ii_kvartale_fakt',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'За АПРЕЛЬ',
|
header: 'За апрель',
|
||||||
accessorKey: 'data.q2.m1',
|
accessorKey: 'data.q2.m1',
|
||||||
columnLetter: 'R',
|
columnLetter: 'R',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -538,7 +538,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'За МАЙ',
|
header: 'За май',
|
||||||
accessorKey: 'data.q2.m2',
|
accessorKey: 'data.q2.m2',
|
||||||
columnLetter: 'S',
|
columnLetter: 'S',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -585,7 +585,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Экономия (+) перерасход (-)',
|
header: 'Экономия (+), перерасход (-)',
|
||||||
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_ii_kvartale_ekonomiya_pereraskhod',
|
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_ii_kvartale_ekonomiya_pereraskhod',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@ -623,7 +623,7 @@ export const config = {
|
|||||||
header: '',
|
header: '',
|
||||||
accessorKey: 'data.q3.carryover',
|
accessorKey: 'data.q3.carryover',
|
||||||
columnLetter: 'X',
|
columnLetter: 'X',
|
||||||
size: 150,
|
size: 250,
|
||||||
filterFn: 'contains',
|
filterFn: 'contains',
|
||||||
muiTableHeadCellProps: {
|
muiTableHeadCellProps: {
|
||||||
sx: {
|
sx: {
|
||||||
@ -708,7 +708,7 @@ export const config = {
|
|||||||
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_iii_kvartale_fakt',
|
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_iii_kvartale_fakt',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'За ИЮЛЬ',
|
header: 'За июль',
|
||||||
accessorKey: 'data.q3.m1',
|
accessorKey: 'data.q3.m1',
|
||||||
columnLetter: 'AB',
|
columnLetter: 'AB',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -721,7 +721,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'За АВГУСТ',
|
header: 'За август',
|
||||||
accessorKey: 'data.q3.m2',
|
accessorKey: 'data.q3.m2',
|
||||||
columnLetter: 'AC',
|
columnLetter: 'AC',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -768,7 +768,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Экономия (+) перерасход (-)',
|
header: 'Экономия (+), перерасход (-)',
|
||||||
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_iii_kvartale_ekonomiya_pereraskhod',
|
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_iii_kvartale_ekonomiya_pereraskhod',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@ -806,7 +806,7 @@ export const config = {
|
|||||||
header: '',
|
header: '',
|
||||||
accessorKey: 'data.q4.carryover',
|
accessorKey: 'data.q4.carryover',
|
||||||
columnLetter: 'AH',
|
columnLetter: 'AH',
|
||||||
size: 150,
|
size: 250,
|
||||||
filterFn: 'contains',
|
filterFn: 'contains',
|
||||||
muiTableHeadCellProps: {
|
muiTableHeadCellProps: {
|
||||||
sx: {
|
sx: {
|
||||||
@ -891,7 +891,7 @@ export const config = {
|
|||||||
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_iv_kvartale_fakt',
|
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_iv_kvartale_fakt',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'За ОКТЯБРЬ',
|
header: 'За октябрь',
|
||||||
accessorKey: 'data.q4.m1',
|
accessorKey: 'data.q4.m1',
|
||||||
columnLetter: 'AL',
|
columnLetter: 'AL',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -904,7 +904,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'За НОЯБРЬ',
|
header: 'За ноябрь',
|
||||||
accessorKey: 'data.q4.m2',
|
accessorKey: 'data.q4.m2',
|
||||||
columnLetter: 'AM',
|
columnLetter: 'AM',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -964,7 +964,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Экономия (+) перерасход (-)',
|
header: 'Экономия (+), перерасход (-)',
|
||||||
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_iv_kvartale_ekonomiya_pereraskhod',
|
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_iv_kvartale_ekonomiya_pereraskhod',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -279,7 +279,7 @@ export const config = {
|
|||||||
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_i_kvartale_korrektirovka_limita',
|
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_i_kvartale_korrektirovka_limita',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'По статьям сметы *',
|
header: 'По статьям сметы*',
|
||||||
accessorKey: 'data.q1.adj_by_items',
|
accessorKey: 'data.q1.adj_by_items',
|
||||||
columnLetter: 'E',
|
columnLetter: 'E',
|
||||||
size: 180,
|
size: 180,
|
||||||
@ -292,7 +292,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Увеличение сметы **',
|
header: 'Увеличение сметы**',
|
||||||
accessorKey: 'data.q1.adj_increase',
|
accessorKey: 'data.q1.adj_increase',
|
||||||
columnLetter: 'F',
|
columnLetter: 'F',
|
||||||
size: 200,
|
size: 200,
|
||||||
@ -342,7 +342,7 @@ export const config = {
|
|||||||
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_i_kvartale_fakt',
|
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_i_kvartale_fakt',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'За ЯНВАРЬ',
|
header: 'За январь',
|
||||||
accessorKey: 'data.q1.m1',
|
accessorKey: 'data.q1.m1',
|
||||||
columnLetter: 'H',
|
columnLetter: 'H',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -355,7 +355,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'За ФЕВРАЛЬ',
|
header: 'За февраль',
|
||||||
accessorKey: 'data.q1.m2',
|
accessorKey: 'data.q1.m2',
|
||||||
columnLetter: 'I',
|
columnLetter: 'I',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -368,7 +368,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'За',
|
header: 'За март',
|
||||||
accessorKey: 'data.q1.m3',
|
accessorKey: 'data.q1.m3',
|
||||||
columnLetter: 'J',
|
columnLetter: 'J',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -402,7 +402,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Экономия (+) перерасход (-)',
|
header: 'Экономия (+), перерасход (-)',
|
||||||
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_i_kvartale_ekonomiya_pereraskhod',
|
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_i_kvartale_ekonomiya_pereraskhod',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@ -441,7 +441,7 @@ export const config = {
|
|||||||
header: '',
|
header: '',
|
||||||
accessorKey: 'data.q2.carryover',
|
accessorKey: 'data.q2.carryover',
|
||||||
columnLetter: 'N',
|
columnLetter: 'N',
|
||||||
size: 150,
|
size: 250,
|
||||||
filterFn: 'contains',
|
filterFn: 'contains',
|
||||||
muiTableHeadCellProps: {
|
muiTableHeadCellProps: {
|
||||||
sx: {
|
sx: {
|
||||||
@ -463,7 +463,7 @@ export const config = {
|
|||||||
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_ii_kvartale_korrektirovka_limita',
|
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_ii_kvartale_korrektirovka_limita',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'По статьям сметы *',
|
header: 'По статьям сметы*',
|
||||||
accessorKey: 'data.q2.adj_by_items',
|
accessorKey: 'data.q2.adj_by_items',
|
||||||
columnLetter: 'O',
|
columnLetter: 'O',
|
||||||
size: 180,
|
size: 180,
|
||||||
@ -476,7 +476,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Увеличение сметы **',
|
header: 'Увеличение сметы**',
|
||||||
accessorKey: 'data.q2.adj_increase',
|
accessorKey: 'data.q2.adj_increase',
|
||||||
columnLetter: 'P',
|
columnLetter: 'P',
|
||||||
size: 200,
|
size: 200,
|
||||||
@ -526,7 +526,7 @@ export const config = {
|
|||||||
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_ii_kvartale_fakt',
|
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_ii_kvartale_fakt',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'За АПРЕЛЬ',
|
header: 'За апрель',
|
||||||
accessorKey: 'data.q2.m1',
|
accessorKey: 'data.q2.m1',
|
||||||
columnLetter: 'R',
|
columnLetter: 'R',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -539,7 +539,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'За МАЙ',
|
header: 'За май',
|
||||||
accessorKey: 'data.q2.m2',
|
accessorKey: 'data.q2.m2',
|
||||||
columnLetter: 'S',
|
columnLetter: 'S',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -552,7 +552,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'За',
|
header: 'За июнь',
|
||||||
accessorKey: 'data.q2.m3',
|
accessorKey: 'data.q2.m3',
|
||||||
columnLetter: 'T',
|
columnLetter: 'T',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -586,7 +586,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Экономия (+) перерасход (-)',
|
header: 'Экономия (+), перерасход (-)',
|
||||||
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_ii_kvartale_ekonomiya_pereraskhod',
|
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_ii_kvartale_ekonomiya_pereraskhod',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@ -625,7 +625,7 @@ export const config = {
|
|||||||
header: '',
|
header: '',
|
||||||
accessorKey: 'data.q3.carryover',
|
accessorKey: 'data.q3.carryover',
|
||||||
columnLetter: 'X',
|
columnLetter: 'X',
|
||||||
size: 150,
|
size: 250,
|
||||||
filterFn: 'contains',
|
filterFn: 'contains',
|
||||||
muiTableHeadCellProps: {
|
muiTableHeadCellProps: {
|
||||||
sx: {
|
sx: {
|
||||||
@ -647,7 +647,7 @@ export const config = {
|
|||||||
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_iii_kvartale_korrektirovka_limita',
|
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_iii_kvartale_korrektirovka_limita',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'По статьям сметы *',
|
header: 'По статьям сметы*',
|
||||||
accessorKey: 'data.q3.adj_by_items',
|
accessorKey: 'data.q3.adj_by_items',
|
||||||
columnLetter: 'Y',
|
columnLetter: 'Y',
|
||||||
size: 180,
|
size: 180,
|
||||||
@ -660,7 +660,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Увеличение сметы **',
|
header: 'Увеличение сметы**',
|
||||||
accessorKey: 'data.q3.adj_increase',
|
accessorKey: 'data.q3.adj_increase',
|
||||||
columnLetter: 'Z',
|
columnLetter: 'Z',
|
||||||
size: 200,
|
size: 200,
|
||||||
@ -710,7 +710,7 @@ export const config = {
|
|||||||
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_iii_kvartale_fakt',
|
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_iii_kvartale_fakt',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'За ИЮЛЬ',
|
header: 'За июль',
|
||||||
accessorKey: 'data.q3.m1',
|
accessorKey: 'data.q3.m1',
|
||||||
columnLetter: 'AB',
|
columnLetter: 'AB',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -723,7 +723,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'За АВГУСТ',
|
header: 'За август',
|
||||||
accessorKey: 'data.q3.m2',
|
accessorKey: 'data.q3.m2',
|
||||||
columnLetter: 'AC',
|
columnLetter: 'AC',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -736,7 +736,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'За',
|
header: 'За сентябрь',
|
||||||
accessorKey: 'data.q3.m3',
|
accessorKey: 'data.q3.m3',
|
||||||
columnLetter: 'AD',
|
columnLetter: 'AD',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -770,7 +770,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Экономия (+) перерасход (-)',
|
header: 'Экономия (+), перерасход (-)',
|
||||||
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_iii_kvartale_ekonomiya_pereraskhod',
|
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_iii_kvartale_ekonomiya_pereraskhod',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@ -809,7 +809,7 @@ export const config = {
|
|||||||
header: '',
|
header: '',
|
||||||
accessorKey: 'data.q4.carryover',
|
accessorKey: 'data.q4.carryover',
|
||||||
columnLetter: 'AH',
|
columnLetter: 'AH',
|
||||||
size: 150,
|
size: 250,
|
||||||
filterFn: 'contains',
|
filterFn: 'contains',
|
||||||
muiTableHeadCellProps: {
|
muiTableHeadCellProps: {
|
||||||
sx: {
|
sx: {
|
||||||
@ -831,7 +831,7 @@ export const config = {
|
|||||||
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_iv_kvartale_korrektirovka_limita',
|
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_iv_kvartale_korrektirovka_limita',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'По статьям сметы *',
|
header: 'По статьям сметы*',
|
||||||
accessorKey: 'data.q4.adj_by_items',
|
accessorKey: 'data.q4.adj_by_items',
|
||||||
columnLetter: 'AI',
|
columnLetter: 'AI',
|
||||||
size: 180,
|
size: 180,
|
||||||
@ -844,7 +844,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Увеличение сметы **',
|
header: 'Увеличение сметы**',
|
||||||
accessorKey: 'data.q4.adj_increase',
|
accessorKey: 'data.q4.adj_increase',
|
||||||
columnLetter: 'AJ',
|
columnLetter: 'AJ',
|
||||||
size: 200,
|
size: 200,
|
||||||
@ -894,7 +894,7 @@ export const config = {
|
|||||||
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_iv_kvartale_fakt',
|
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_iv_kvartale_fakt',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'За ОКТЯБРЬ',
|
header: 'За октябрь',
|
||||||
accessorKey: 'data.q4.m1',
|
accessorKey: 'data.q4.m1',
|
||||||
columnLetter: 'AL',
|
columnLetter: 'AL',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -907,7 +907,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'За НОЯБРЬ',
|
header: 'За ноябрь',
|
||||||
accessorKey: 'data.q4.m2',
|
accessorKey: 'data.q4.m2',
|
||||||
columnLetter: 'AM',
|
columnLetter: 'AM',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -920,7 +920,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'За',
|
header: 'За декабрь',
|
||||||
accessorKey: 'data.q4.m3',
|
accessorKey: 'data.q4.m3',
|
||||||
columnLetter: 'AN',
|
columnLetter: 'AN',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -967,7 +967,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Экономия (+) перерасход (-)',
|
header: 'Экономия (+), перерасход (-)',
|
||||||
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_iv_kvartale_ekonomiya_pereraskhod',
|
accessorKey: 'otchet_ob_ispolnenii_limita_zatrat_utverzhdennogo_pravleniem_banka_v_iv_kvartale_ekonomiya_pereraskhod',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -46,6 +46,14 @@ export const DIRECTION_TRANSLATE = { Support: 'Поддержка', Development:
|
|||||||
|
|
||||||
export const PROJECT_TYPES = ['Открытие ВСП', 'Закрытие ВСП', 'Переезд ВСП', 'Реновация РФ', 'Открытие УРМ'];
|
export const PROJECT_TYPES = ['Открытие ВСП', 'Закрытие ВСП', 'Переезд ВСП', 'Реновация РФ', 'Открытие УРМ'];
|
||||||
|
|
||||||
|
export const PROJECT_STATUSES = {
|
||||||
|
created: 'Создан',
|
||||||
|
agreed: 'Согласован',
|
||||||
|
approved: 'Согласован',
|
||||||
|
archived: 'В архиве',
|
||||||
|
deleted: 'Удалён',
|
||||||
|
};
|
||||||
|
|
||||||
export const ORG_UNIT_TYPE_OPTIONS = [
|
export const ORG_UNIT_TYPE_OPTIONS = [
|
||||||
{ value: 'ssp', label: 'ССП' },
|
{ value: 'ssp', label: 'ССП' },
|
||||||
{ value: 'rf', label: 'Региональный филиал' },
|
{ value: 'rf', label: 'Региональный филиал' },
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { DEVELOMENT_BLOCK, FUNDING_BY_DECISION } from '../../../ProjectPages/con
|
|||||||
import {
|
import {
|
||||||
DIRECTION_TRANSLATE,
|
DIRECTION_TRANSLATE,
|
||||||
FORM_TYPE_TRANSLATE,
|
FORM_TYPE_TRANSLATE,
|
||||||
|
PROJECT_STATUSES,
|
||||||
ROLES_ID_RUSSIAN_NAME,
|
ROLES_ID_RUSSIAN_NAME,
|
||||||
SHEET_NAME,
|
SHEET_NAME,
|
||||||
STAGE_ROLE_RUSSIAN_NAME,
|
STAGE_ROLE_RUSSIAN_NAME,
|
||||||
@ -50,14 +51,6 @@ export const VALUE_TRANSFORMERS = {
|
|||||||
'Дата открытия / переезда / закрытия': transformDateChange,
|
'Дата открытия / переезда / закрытия': transformDateChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PROJECT_STATUSES = {
|
|
||||||
created: 'Создан',
|
|
||||||
agreed: 'Согласован',
|
|
||||||
approved: 'Согласован',
|
|
||||||
archived: 'В архиве',
|
|
||||||
deleted: 'Удалён',
|
|
||||||
};
|
|
||||||
|
|
||||||
const PLACEMENT_TYPES = {
|
const PLACEMENT_TYPES = {
|
||||||
own: 'Собственность',
|
own: 'Собственность',
|
||||||
rent: 'Аренда',
|
rent: 'Аренда',
|
||||||
|
|||||||
@ -42,6 +42,15 @@ const SummaryPage = () => {
|
|||||||
// Состояния для фильтров
|
// Состояния для фильтров
|
||||||
const [selectedBranch, setSelectedBranch] = useState('');
|
const [selectedBranch, setSelectedBranch] = useState('');
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [selectedStatus, setSelectedStatus] = useState('');
|
||||||
|
const [archivedData, setArchivedData] = useState([]);
|
||||||
|
const [archivedTotalCount, setArchivedTotalCount] = useState(0);
|
||||||
|
const [archivedPagination, setArchivedPagination] = useState({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isArchiveMode = selectedStatus === 'archived';
|
||||||
|
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
@ -94,10 +103,7 @@ const SummaryPage = () => {
|
|||||||
const loadProjects = async () => {
|
const loadProjects = async () => {
|
||||||
setIsProjectsLoading(true);
|
setIsProjectsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await ProjectsApi.listWithReports({
|
const response = await ProjectsApi.listWithReports({ limit: 1000 });
|
||||||
limit: 1000,
|
|
||||||
...(selectedBranch?.id ? { branch_id: selectedBranch.id } : {}),
|
|
||||||
});
|
|
||||||
if (active) setTableData(response.result || []);
|
if (active) setTableData(response.result || []);
|
||||||
} catch (requestError) {
|
} catch (requestError) {
|
||||||
console.error('Error loading projects with reports:', requestError);
|
console.error('Error loading projects with reports:', requestError);
|
||||||
@ -110,7 +116,75 @@ const SummaryPage = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
active = false;
|
active = false;
|
||||||
};
|
};
|
||||||
}, [user, isLoading, selectedBranch?.id, projectsReloadKey]);
|
}, [user, isLoading, projectsReloadKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
if (!user || isLoading || !isArchiveMode) return undefined;
|
||||||
|
|
||||||
|
const loadArchivedProjects = async () => {
|
||||||
|
setIsProjectsLoading(true);
|
||||||
|
try {
|
||||||
|
const offset = archivedPagination.pageIndex * archivedPagination.pageSize;
|
||||||
|
const params = {
|
||||||
|
status: 'archived',
|
||||||
|
offset,
|
||||||
|
limit: archivedPagination.pageSize,
|
||||||
|
...(selectedBranch?.id ? { branch_id: selectedBranch.id } : {}),
|
||||||
|
...(searchQuery.trim() ? { search: searchQuery.trim() } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: после реализации серверных фильтров и пагинации достаточно
|
||||||
|
// передать params в listWithReports и использовать response.result/count.
|
||||||
|
// Пока запрос возвращает полный набор, а код ниже имитирует ответ бэка.
|
||||||
|
const response = await ProjectsApi.listWithReports(params);
|
||||||
|
const query = searchQuery.toLowerCase().trim();
|
||||||
|
const filtered = (response.result || [])
|
||||||
|
.filter((item) => item.status === 'archived')
|
||||||
|
.filter((item) => !selectedBranch || item.org_unit_id === selectedBranch.id)
|
||||||
|
.map((item) => {
|
||||||
|
if (!query) return item;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
sub_rows: item.sub_rows?.filter((subItem) =>
|
||||||
|
(subItem.project || '').toLowerCase().includes(query),
|
||||||
|
) || [],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item) => !query
|
||||||
|
|| (item.name || '').toLowerCase().includes(query)
|
||||||
|
|| item.sub_rows.length > 0);
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
setArchivedData(filtered.slice(offset, offset + archivedPagination.pageSize));
|
||||||
|
setArchivedTotalCount(filtered.length);
|
||||||
|
}
|
||||||
|
} catch (requestError) {
|
||||||
|
console.error('Error loading archived projects:', requestError);
|
||||||
|
if (active) {
|
||||||
|
setArchivedData([]);
|
||||||
|
setArchivedTotalCount(0);
|
||||||
|
toast.error('Не удалось загрузить архивные проекты');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (active) setIsProjectsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadArchivedProjects();
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
user,
|
||||||
|
isLoading,
|
||||||
|
isArchiveMode,
|
||||||
|
selectedBranch,
|
||||||
|
searchQuery,
|
||||||
|
archivedPagination.pageIndex,
|
||||||
|
archivedPagination.pageSize,
|
||||||
|
projectsReloadKey,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let active = true;
|
let active = true;
|
||||||
@ -143,12 +217,19 @@ const SummaryPage = () => {
|
|||||||
}, [selectedRow?.original?.id]);
|
}, [selectedRow?.original?.id]);
|
||||||
|
|
||||||
const getFilteredData = () => {
|
const getFilteredData = () => {
|
||||||
if (!selectedBranch && !searchQuery) {
|
if (isArchiveMode) {
|
||||||
|
return archivedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedBranch && !searchQuery && !selectedStatus) {
|
||||||
return tableData;
|
return tableData;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tableData
|
return tableData
|
||||||
.filter((item) => {
|
.filter((item) => {
|
||||||
|
if (selectedStatus && item.status !== selectedStatus) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Фильтр по филиалу (только для корневых)
|
// Фильтр по филиалу (только для корневых)
|
||||||
if (selectedBranch) {
|
if (selectedBranch) {
|
||||||
return item.org_unit_id === selectedBranch.id;
|
return item.org_unit_id === selectedBranch.id;
|
||||||
@ -315,9 +396,10 @@ const SummaryPage = () => {
|
|||||||
const projectsTableOptions = {
|
const projectsTableOptions = {
|
||||||
enableColumnActions: false,
|
enableColumnActions: false,
|
||||||
enableColumnFilters: false,
|
enableColumnFilters: false,
|
||||||
enablePagination: false,
|
enablePagination: isArchiveMode,
|
||||||
|
manualPagination: isArchiveMode,
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
enableBottomToolbar: false,
|
enableBottomToolbar: isArchiveMode,
|
||||||
enableTopToolbar: false,
|
enableTopToolbar: false,
|
||||||
enableExpanding: true,
|
enableExpanding: true,
|
||||||
getSubRows: (row) => row.sub_rows,
|
getSubRows: (row) => row.sub_rows,
|
||||||
@ -390,7 +472,7 @@ const SummaryPage = () => {
|
|||||||
border: '1px solid #e0e0e0',
|
border: '1px solid #e0e0e0',
|
||||||
borderRadius: '1rem',
|
borderRadius: '1rem',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
height: '45vh',
|
height: isArchiveMode ? 'auto' : '45vh',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
},
|
},
|
||||||
@ -398,7 +480,8 @@ const SummaryPage = () => {
|
|||||||
muiTableContainerProps: {
|
muiTableContainerProps: {
|
||||||
sx: {
|
sx: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
overflow: 'auto',
|
overflowX: 'auto',
|
||||||
|
overflowY: isArchiveMode ? 'visible' : 'auto',
|
||||||
'& thead': {
|
'& thead': {
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
top: 0,
|
top: 0,
|
||||||
@ -484,7 +567,14 @@ const SummaryPage = () => {
|
|||||||
initialState: {
|
initialState: {
|
||||||
columnPinning: { right: ['actions'], left: ['mrt-row-expand', 'project'] },
|
columnPinning: { right: ['actions'], left: ['mrt-row-expand', 'project'] },
|
||||||
},
|
},
|
||||||
state: { isLoading: isLoading || isProjectsLoading },
|
...(isArchiveMode ? {
|
||||||
|
rowCount: archivedTotalCount,
|
||||||
|
onPaginationChange: setArchivedPagination,
|
||||||
|
} : {}),
|
||||||
|
state: {
|
||||||
|
isLoading: isLoading || isProjectsLoading,
|
||||||
|
...(isArchiveMode ? { pagination: archivedPagination } : {}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const summaryProjectTable = useMaterialReactTable({
|
const summaryProjectTable = useMaterialReactTable({
|
||||||
@ -493,6 +583,8 @@ const SummaryPage = () => {
|
|||||||
...summaryProjectTableOptions,
|
...summaryProjectTableOptions,
|
||||||
localization: {
|
localization: {
|
||||||
noRecordsToDisplay: 'Нет данных для отображения',
|
noRecordsToDisplay: 'Нет данных для отображения',
|
||||||
|
rowsPerPage: 'Строк на странице',
|
||||||
|
of: 'из',
|
||||||
},
|
},
|
||||||
initialState: {
|
initialState: {
|
||||||
columnPinning: { left: ['data.header.name'] },
|
columnPinning: { left: ['data.header.name'] },
|
||||||
@ -512,10 +604,19 @@ const SummaryPage = () => {
|
|||||||
setSelectedRow(null);
|
setSelectedRow(null);
|
||||||
setSummaryData([]);
|
setSummaryData([]);
|
||||||
setSelectedBranch(value);
|
setSelectedBranch(value);
|
||||||
|
setArchivedPagination((current) => ({ ...current, pageIndex: 0 }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchChange = (value) => {
|
const handleSearchChange = (value) => {
|
||||||
setSearchQuery(value);
|
setSearchQuery(value);
|
||||||
|
setArchivedPagination((current) => ({ ...current, pageIndex: 0 }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStatusChange = (value) => {
|
||||||
|
setSelectedRow(null);
|
||||||
|
setSummaryData([]);
|
||||||
|
setSelectedStatus(value);
|
||||||
|
setArchivedPagination((current) => ({ ...current, pageIndex: 0 }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateProject = async (formData) => {
|
const handleCreateProject = async (formData) => {
|
||||||
@ -559,6 +660,8 @@ const SummaryPage = () => {
|
|||||||
onBranchChange={handleBranchChange}
|
onBranchChange={handleBranchChange}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
|
selectedStatus={selectedStatus}
|
||||||
|
onStatusChange={handleStatusChange}
|
||||||
/>
|
/>
|
||||||
<PrimaryButton onClick={() => setCreateModalOpen(true)} startIcon={<WhitePlus />} sx={{ height: '2.5rem' }}>
|
<PrimaryButton onClick={() => setCreateModalOpen(true)} startIcon={<WhitePlus />} sx={{ height: '2.5rem' }}>
|
||||||
Создать проект
|
Создать проект
|
||||||
@ -570,7 +673,7 @@ const SummaryPage = () => {
|
|||||||
sx={{
|
sx={{
|
||||||
mb: '2rem',
|
mb: '2rem',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
flex: '0 0 45vh',
|
flex: isArchiveMode ? '0 0 auto' : '0 0 45vh',
|
||||||
borderRadius: '1rem',
|
borderRadius: '1rem',
|
||||||
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
|
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@ -3,10 +3,12 @@ import { Box, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Men
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useAuth } from '../../../../app/context/AuthProvider';
|
import { useAuth } from '../../../../app/context/AuthProvider';
|
||||||
import { DangerOutlinedButton, PrimaryButton } from '../../../../components/common/Buttons/Buttons';
|
import { DangerOutlinedButton, PrimaryButton } from '../../../../components/common/Buttons/Buttons';
|
||||||
import { PROJECT_TYPES } from '../../../../constants/constants';
|
import { PROJECT_STATUSES, PROJECT_TYPES } from '../../../../constants/constants';
|
||||||
import { DEVELOMENT_BLOCK, FUNDING_BY_DECISION } from '../../constants';
|
import { DEVELOMENT_BLOCK, FUNDING_BY_DECISION } from '../../constants';
|
||||||
import { canEditFirstTableField, hasReserveValuesInChildSmetas, isFirstTableFieldDependencySatisfied } from '../../constants/fieldAccess';
|
import { canEditFirstTableField, hasReserveValuesInChildSmetas, isFirstTableFieldDependencySatisfied } from '../../constants/fieldAccess';
|
||||||
|
|
||||||
|
const PROJECT_STATUS_OPTIONS = ['created', 'agreed', 'archived'].map((status) => [status, PROJECT_STATUSES[status]]);
|
||||||
|
|
||||||
const EditModal = ({ open, onClose, rowData, onSave, isSaving = false, orgUnitNames = {} }) => {
|
const EditModal = ({ open, onClose, rowData, onSave, isSaving = false, orgUnitNames = {} }) => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [formData, setFormData] = React.useState(rowData || {});
|
const [formData, setFormData] = React.useState(rowData || {});
|
||||||
@ -54,7 +56,7 @@ const EditModal = ({ open, onClose, rowData, onSave, isSaving = false, orgUnitNa
|
|||||||
|
|
||||||
const projectFields = [
|
const projectFields = [
|
||||||
{ key: 'name', label: 'Проект' },
|
{ key: 'name', label: 'Проект' },
|
||||||
{ key: 'status', label: 'Статус', options: [['created', 'Создан'], ['agreed', 'Согласован'], ['archived', 'В архиве']] },
|
{ key: 'status', label: 'Статус', options: PROJECT_STATUS_OPTIONS },
|
||||||
{ key: 'technical_number', label: 'Технический номер проекта' },
|
{ key: 'technical_number', label: 'Технический номер проекта' },
|
||||||
{ key: 'org_unit_id', label: 'ССП/РФ', valueType: 'number', options: Object.entries(orgUnitNames).map(([id, title]) => [Number(id), title]) },
|
{ key: 'org_unit_id', label: 'ССП/РФ', valueType: 'number', options: Object.entries(orgUnitNames).map(([id, title]) => [Number(id), title]) },
|
||||||
{ key: 'project_type', label: 'Тип проекта', options: PROJECT_TYPES },
|
{ key: 'project_type', label: 'Тип проекта', options: PROJECT_TYPES },
|
||||||
|
|||||||
@ -1,26 +1,22 @@
|
|||||||
|
import { PROJECT_STATUSES } from '../../../../constants/constants';
|
||||||
import styles from './StatusBadge.module.css';
|
import styles from './StatusBadge.module.css';
|
||||||
|
|
||||||
const StatusBadge = ({ status, className = '', ...props }) => {
|
const StatusBadge = ({ status, className = '', ...props }) => {
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
deleted: {
|
deleted: {
|
||||||
class: styles.deleted,
|
class: styles.deleted,
|
||||||
label: 'Удален',
|
|
||||||
},
|
},
|
||||||
approved: {
|
approved: {
|
||||||
class: styles.approved,
|
class: styles.approved,
|
||||||
label: 'Согласован',
|
|
||||||
},
|
},
|
||||||
agreed: {
|
agreed: {
|
||||||
class: styles.approved,
|
class: styles.approved,
|
||||||
label: 'Согласован',
|
|
||||||
},
|
},
|
||||||
archived: {
|
archived: {
|
||||||
class: styles.archived,
|
class: styles.archived,
|
||||||
label: 'В архиве',
|
|
||||||
},
|
},
|
||||||
created: {
|
created: {
|
||||||
class: styles.created,
|
class: styles.created,
|
||||||
label: 'Создан',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -31,7 +27,7 @@ const StatusBadge = ({ status, className = '', ...props }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={`${styles.badge} ${currentStatus.class} ${className}`} {...props}>
|
<span className={`${styles.badge} ${currentStatus.class} ${className}`} {...props}>
|
||||||
{currentStatus.label}
|
{PROJECT_STATUSES[status]}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,12 +1,21 @@
|
|||||||
import { Search } from '@mui/icons-material';
|
import { Search } from '@mui/icons-material';
|
||||||
import { Autocomplete, Box, Chip, CircularProgress, Paper, TextField } from '@mui/material';
|
import { Autocomplete, Box, Chip, CircularProgress, MenuItem, Paper, TextField } from '@mui/material';
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import { SspApi } from '../../../../api/ssp';
|
import { SspApi } from '../../../../api/ssp';
|
||||||
import { useAuth } from '../../../../app/context/AuthProvider';
|
import { useAuth } from '../../../../app/context/AuthProvider';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { ROLES_NAME_ID } from '../../../../constants/constants';
|
import { PROJECT_STATUSES, ROLES_NAME_ID } from '../../../../constants/constants';
|
||||||
|
|
||||||
const TableFilters = ({ onBranchChange, onSearchChange, selectedBranch, searchQuery }) => {
|
const FILTER_PROJECT_STATUSES = ['created', 'agreed', 'archived'];
|
||||||
|
|
||||||
|
const TableFilters = ({
|
||||||
|
onBranchChange,
|
||||||
|
onSearchChange,
|
||||||
|
onStatusChange,
|
||||||
|
selectedBranch,
|
||||||
|
selectedStatus,
|
||||||
|
searchQuery,
|
||||||
|
}) => {
|
||||||
const [branches, setBranches] = useState([]);
|
const [branches, setBranches] = useState([]);
|
||||||
const [branchInputValue, setBranchInputValue] = useState('');
|
const [branchInputValue, setBranchInputValue] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@ -138,7 +147,26 @@ const TableFilters = ({ onBranchChange, onSearchChange, selectedBranch, searchQu
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{(selectedBranch || searchQuery) && (
|
<TextField
|
||||||
|
select
|
||||||
|
size='small'
|
||||||
|
label='Статус'
|
||||||
|
value={selectedStatus}
|
||||||
|
onChange={(event) => onStatusChange(event.target.value)}
|
||||||
|
sx={{
|
||||||
|
minWidth: 180,
|
||||||
|
maxWidth: 220,
|
||||||
|
flex: '1 1 auto',
|
||||||
|
}}>
|
||||||
|
<MenuItem value=''>Все статусы</MenuItem>
|
||||||
|
{FILTER_PROJECT_STATUSES.map((status) => (
|
||||||
|
<MenuItem key={status} value={status}>
|
||||||
|
{PROJECT_STATUSES[status]}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
|
||||||
|
{(selectedBranch || searchQuery || selectedStatus) && (
|
||||||
<Box sx={{ display: 'flex', gap: 1, ml: 'auto' }}>
|
<Box sx={{ display: 'flex', gap: 1, ml: 'auto' }}>
|
||||||
<Chip
|
<Chip
|
||||||
label='Сбросить фильтры'
|
label='Сбросить фильтры'
|
||||||
@ -147,6 +175,7 @@ const TableFilters = ({ onBranchChange, onSearchChange, selectedBranch, searchQu
|
|||||||
setBranchInputValue('');
|
setBranchInputValue('');
|
||||||
onBranchChange('');
|
onBranchChange('');
|
||||||
onSearchChange('');
|
onSearchChange('');
|
||||||
|
onStatusChange('');
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import TablesList from '../../components/common/TableList/TableList';
|
|||||||
import { TableIcon } from '../../components/common/icons/icons';
|
import { TableIcon } from '../../components/common/icons/icons';
|
||||||
import { Chip } from '../../components/styles/StyledChip';
|
import { Chip } from '../../components/styles/StyledChip';
|
||||||
import { FORM_TYPE_TRANSLATE, SHEET_NAME } from '../../constants/constants';
|
import { FORM_TYPE_TRANSLATE, SHEET_NAME } from '../../constants/constants';
|
||||||
import { exportSingleForm } from '../../utils/exportFile';
|
import { exportProject, exportSingleForm } from '../../utils/exportFile';
|
||||||
|
|
||||||
const TaskInfo = ({ task, isProject }) => {
|
const TaskInfo = ({ task, isProject }) => {
|
||||||
const portalContent = (
|
const portalContent = (
|
||||||
@ -113,7 +113,12 @@ export default function TaskPage() {
|
|||||||
|
|
||||||
// Функция для экспорта
|
// Функция для экспорта
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
const fileName = isProject ? `project_${task?.title || id}` : `form_${task?.title || taskId}`;
|
if (isProject) {
|
||||||
|
const fileName = `project_${task?.name || id}`;
|
||||||
|
exportProject(id, fileName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fileName = `form_${task?.title || taskId}`;
|
||||||
exportSingleForm(id, fileName);
|
exportSingleForm(id, fileName);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -37,6 +37,16 @@ export const exportSheetProject = async (formId, sheetName, year) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const exportProject = async (projectId, projectTitle) => {
|
||||||
|
try {
|
||||||
|
const response = await ProjectsApi.exportProject(projectId);
|
||||||
|
await downloadFile(response, projectTitle, 'xlsx');
|
||||||
|
} catch (error) {
|
||||||
|
const message = await getApiErrorMessage(error, 'Ошибка при скачивании файла');
|
||||||
|
toast.error(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const exportBulkForms = async (formIds, fileName = 'Задачи') => {
|
export const exportBulkForms = async (formIds, fileName = 'Задачи') => {
|
||||||
try {
|
try {
|
||||||
const response = await TasksApi.exportTasks({ form_ids: formIds });
|
const response = await TasksApi.exportTasks({ form_ids: formIds });
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user