row_json_select(strip_nulls) оборачивает data в jsonb_strip_nulls внутри json_build_object; флаг пробрасывается через get_json/get_svod1_json/get_svod2_json, роутеры листа и сводов включают его. Проекты, PATCH/POST/DELETE не затронуты. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
239 lines
7.5 KiB
Python
239 lines
7.5 KiB
Python
from typing import Any
|
|
|
|
from sqlalchemy import text
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from src.repository.row_json import ROW_COLUMNS, fetch_row_texts, row_json_select
|
|
|
|
|
|
class SummaryRepository:
|
|
def __init__(self, db: AsyncSession):
|
|
self.db = db
|
|
|
|
@staticmethod
|
|
def _svod1_source() -> str:
|
|
return (
|
|
"FROM v3.v_summary_view("
|
|
"CAST(:year AS INT), "
|
|
"CAST(:sheet AS TEXT), "
|
|
"CAST(:org_ids AS INT[]))"
|
|
)
|
|
|
|
@staticmethod
|
|
def _svod2_source(sheet: str) -> str:
|
|
function_name = {
|
|
"GO": "v3.v_svod2_go",
|
|
"RF": "v3.v_svod2_rf",
|
|
}[sheet]
|
|
return (
|
|
f"FROM {function_name}("
|
|
"CAST(:year AS INT), "
|
|
"CAST(:org_ids AS INT[]), "
|
|
"CAST(:offset AS INT), "
|
|
"CAST(:limit AS INT), "
|
|
"CAST(:sort_by AS VARCHAR), "
|
|
"CAST(:sort_direction AS VARCHAR))"
|
|
)
|
|
|
|
@staticmethod
|
|
def _svod2_params(
|
|
year: int,
|
|
org_ids: list[int] | None,
|
|
offset: int | None,
|
|
limit: int | None,
|
|
sort_by: str,
|
|
sort_direction: str,
|
|
) -> dict:
|
|
return {
|
|
"year": year,
|
|
"org_ids": org_ids,
|
|
"offset": offset,
|
|
"limit": limit,
|
|
"sort_by": sort_by,
|
|
"sort_direction": sort_direction,
|
|
}
|
|
|
|
async def get_svod1_rows(
|
|
self,
|
|
year: int,
|
|
sheet: str,
|
|
org_ids: list[int] | None = None,
|
|
) -> list[tuple]:
|
|
query = text(f"SELECT {ROW_COLUMNS} {self._svod1_source()}")
|
|
return (
|
|
await self.db.execute(
|
|
query,
|
|
{"year": year, "sheet": sheet, "org_ids": org_ids},
|
|
)
|
|
).all()
|
|
|
|
async def get_svod1_json(
|
|
self,
|
|
year: int,
|
|
sheet: str,
|
|
org_ids: list[int] | None = None,
|
|
strip_nulls: bool = False,
|
|
) -> list[str]:
|
|
sql = f"{row_json_select(strip_nulls=strip_nulls)} {self._svod1_source()}"
|
|
return await fetch_row_texts(
|
|
self.db,
|
|
sql,
|
|
{"year": year, "sheet": sheet, "org_ids": org_ids},
|
|
)
|
|
|
|
async def get_svod2_rows(
|
|
self,
|
|
year: int,
|
|
sheet: str,
|
|
org_ids: list[int] | None = None,
|
|
offset: int | None = None,
|
|
limit: int | None = None,
|
|
sort_by: str = "org_name",
|
|
sort_direction: str = "asc",
|
|
) -> list[tuple]:
|
|
query = text(
|
|
f"SELECT 'INPUT'::VARCHAR AS row_type, 3 AS depth, sort_order, data "
|
|
f"{self._svod2_source(sheet)}"
|
|
)
|
|
return (
|
|
await self.db.execute(
|
|
query,
|
|
self._svod2_params(year, org_ids, offset, limit, sort_by, sort_direction),
|
|
)
|
|
).all()
|
|
|
|
async def get_svod2_json(
|
|
self,
|
|
year: int,
|
|
sheet: str,
|
|
org_ids: list[int] | None = None,
|
|
offset: int | None = None,
|
|
limit: int | None = None,
|
|
sort_by: str = "org_name",
|
|
sort_direction: str = "asc",
|
|
strip_nulls: bool = False,
|
|
) -> list[str]:
|
|
select = row_json_select(row_type="'INPUT'::VARCHAR", depth="3", strip_nulls=strip_nulls)
|
|
return await fetch_row_texts(
|
|
self.db,
|
|
f"{select} {self._svod2_source(sheet)}",
|
|
self._svod2_params(year, org_ids, offset, limit, sort_by, sort_direction),
|
|
)
|
|
|
|
async def get_svod2_go_export_rows(
|
|
self,
|
|
year: int,
|
|
org_ids: list[int] | None,
|
|
sort_by: str,
|
|
sort_direction: str,
|
|
) -> list[tuple]:
|
|
"""Load the compact flat representation produced for XLSX export."""
|
|
common_quarter_fields = (
|
|
"adj_current", "adj_ssp", "adj_rf", "adj_reserve",
|
|
"payment_date", "payment_amount", "payment_amount_ho",
|
|
"payment_amount_rf", "payment_act", "booking_amount",
|
|
"actual_m1", "actual_m2", "actual_m3",
|
|
)
|
|
export_keys = [
|
|
"header.smeta_type",
|
|
"header.smeta_direction",
|
|
"header.org_name",
|
|
"header.name",
|
|
"header.item_id",
|
|
"header.num_group",
|
|
"header.justification",
|
|
*(f"plan.plan_q{quarter}" for quarter in range(1, 5)),
|
|
"allocation.internal_order",
|
|
"allocation.property_object",
|
|
*(f"sequestration.DFIP.adj_q{quarter}" for quarter in range(1, 5)),
|
|
*(f"reserve.amount_q{quarter}" for quarter in range(1, 5)),
|
|
*(f"approved.approved_q{quarter}" for quarter in range(1, 5)),
|
|
*(
|
|
f"contract_detail.{field}"
|
|
for field in (
|
|
"counterparty", "reference", "subject", "currency",
|
|
"ceiling_amount", "expenses_q1", "expenses_q2",
|
|
"expenses_q3", "expenses_q4", "vat_rate", "deadline",
|
|
)
|
|
),
|
|
]
|
|
for quarter in range(1, 5):
|
|
quarter_fields = list(common_quarter_fields)
|
|
if quarter == 4:
|
|
quarter_fields.append("actual_spod")
|
|
if quarter > 1:
|
|
quarter_fields.extend(
|
|
(
|
|
"plan_revision_eco_change",
|
|
"plan_revision_item_adj",
|
|
"plan_revision_increase",
|
|
"plan_revision_sequester",
|
|
)
|
|
)
|
|
quarter_fields.extend(
|
|
f"transfer_to_q{target_quarter}"
|
|
for target_quarter in range(quarter + 1, 5)
|
|
)
|
|
if quarter < 4:
|
|
quarter_fields.append("transfer_to_economy")
|
|
export_keys.extend(f"q{quarter}.{field}" for field in quarter_fields)
|
|
|
|
query = text(
|
|
"""
|
|
SELECT sort_order, row_values
|
|
FROM v3.v_svod2_go_export(
|
|
CAST(:year AS INT),
|
|
CAST(:org_ids AS INT[]),
|
|
CAST(:sort_by AS VARCHAR),
|
|
CAST(:sort_direction AS VARCHAR)
|
|
)
|
|
"""
|
|
)
|
|
result = await self.db.execute(
|
|
query,
|
|
{
|
|
"year": year,
|
|
"org_ids": org_ids,
|
|
"sort_by": sort_by,
|
|
"sort_direction": sort_direction.lower(),
|
|
},
|
|
)
|
|
rows = []
|
|
for row in result.mappings():
|
|
data = {
|
|
key: self._parse_svod2_export_value(key, value)
|
|
for key, value in zip(export_keys, row["row_values"])
|
|
}
|
|
rows.append(("INPUT", 3, row["sort_order"], data))
|
|
return rows
|
|
|
|
@staticmethod
|
|
def _parse_svod2_export_value(key: str, value: str | None) -> Any:
|
|
if value is None:
|
|
return None
|
|
numeric_prefixes = (
|
|
"plan.",
|
|
"sequestration.",
|
|
"reserve.",
|
|
"approved.",
|
|
)
|
|
numeric_contract_fields = {
|
|
"contract_detail.ceiling_amount",
|
|
"contract_detail.expenses_q1",
|
|
"contract_detail.expenses_q2",
|
|
"contract_detail.expenses_q3",
|
|
"contract_detail.expenses_q4",
|
|
}
|
|
quarter_text_suffixes = (".payment_date", ".payment_act")
|
|
is_numeric_quarter = (
|
|
key.startswith(("q1.", "q2.", "q3.", "q4."))
|
|
and not key.endswith(quarter_text_suffixes)
|
|
)
|
|
if (
|
|
key.startswith(numeric_prefixes)
|
|
or key in numeric_contract_fields
|
|
or is_numeric_quarter
|
|
):
|
|
return float(value)
|
|
return value
|