From 60939a2cf2463c8197884a5c6641bce2873b42cc Mon Sep 17 00:00:00 2001 From: tsygankoviva Date: Wed, 6 May 2026 13:16:26 +0300 Subject: [PATCH] test-tsygankov --- api/requirements.txt | Bin 1542 -> 1626 bytes api/src/app.py | 20 ++- api/src/conf/settings.py | 1 + api/src/data/__init__.py | 0 api/src/data/budget_line.py | 66 ++++++++ api/src/data/expense_item.py | 17 ++ api/src/data/page.py | 45 ++++++ api/src/db/__init__.py | 0 api/src/db/base.py | 42 +++++ api/src/db/models/__init__.py | 5 + api/src/db/models/budget_form.py | 24 +++ api/src/db/models/budget_line.py | 19 +++ api/src/db/models/expense_item.py | 40 +++++ api/src/db/session.py | 26 +++ api/src/exceptions.py | 21 +++ api/src/routes/api/form.py | 73 +++++++++ api/src/routes/api/route.py | 6 +- api/src/routes/api/schemas.py | 7 - api/src/routes/api/schemas/__init__.py | 1 + api/src/routes/api/schemas/base.py | 22 +++ api/src/routes/api/schemas/budget_line.py | 40 +++++ api/src/services/__init__.py | 0 api/src/services/budget_line.py | 187 ++++++++++++++++++++++ api/src/services/page.py | 53 ++++++ 24 files changed, 706 insertions(+), 9 deletions(-) create mode 100644 api/src/data/__init__.py create mode 100644 api/src/data/budget_line.py create mode 100644 api/src/data/expense_item.py create mode 100644 api/src/data/page.py create mode 100644 api/src/db/__init__.py create mode 100644 api/src/db/base.py create mode 100644 api/src/db/models/__init__.py create mode 100644 api/src/db/models/budget_form.py create mode 100644 api/src/db/models/budget_line.py create mode 100644 api/src/db/models/expense_item.py create mode 100644 api/src/db/session.py create mode 100644 api/src/exceptions.py create mode 100644 api/src/routes/api/form.py delete mode 100644 api/src/routes/api/schemas.py create mode 100644 api/src/routes/api/schemas/__init__.py create mode 100644 api/src/routes/api/schemas/base.py create mode 100644 api/src/routes/api/schemas/budget_line.py create mode 100644 api/src/services/__init__.py create mode 100644 api/src/services/budget_line.py create mode 100644 api/src/services/page.py diff --git a/api/requirements.txt b/api/requirements.txt index 89b03ec9c145584419b94913e615b19f37c7e133..445ee496479677e74c6bdb89f46d897bc10cdafc 100644 GIT binary patch delta 86 zcmZqUxy3WVPco6An4yv(k0F_%fFYg176=U(^caj83^uwxX3;HXC}hZCNCe_!h75*O chFqXpJD?gPpc;reBL*D?8!+E+vNG#q05OOW5&!@I delta 14 Vcmcb`)5bHwZ)3qNmdQ1&_W&+121oz^ diff --git a/api/src/app.py b/api/src/app.py index 5a8b160..49e416a 100644 --- a/api/src/app.py +++ b/api/src/app.py @@ -1,12 +1,14 @@ import os from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from datetime import datetime, timezone from .conf import get_settings +from .exceptions import NotFound, PermissionDenied, ValidationError from .routes import api_route settings = get_settings() @@ -20,6 +22,22 @@ app = FastAPI( root_path=settings.ROOT_PATH, ) + +@app.exception_handler(NotFound) +async def not_found_handler(request: Request, exc: NotFound): + return JSONResponse(status_code=404, content={"detail": exc.message}) + + +@app.exception_handler(PermissionDenied) +async def permission_denied_handler(request: Request, exc: PermissionDenied): + return JSONResponse(status_code=403, content={"detail": exc.message}) + + +@app.exception_handler(ValidationError) +async def validation_error_handler(request: Request, exc: ValidationError): + return JSONResponse(status_code=422, content={"detail": exc.message}) + + @app.get( "/healthcheck", status_code=200 diff --git a/api/src/conf/settings.py b/api/src/conf/settings.py index 1d96d08..20677af 100644 --- a/api/src/conf/settings.py +++ b/api/src/conf/settings.py @@ -10,6 +10,7 @@ from functools import lru_cache class Settings(BaseSettings): """ Настройки приложения """ ROOT_PATH: str = '/' + DATABASE_URL: str = '' server: ServerSettings = ServerSettings() diff --git a/api/src/data/__init__.py b/api/src/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/src/data/budget_line.py b/api/src/data/budget_line.py new file mode 100644 index 0000000..cee54a3 --- /dev/null +++ b/api/src/data/budget_line.py @@ -0,0 +1,66 @@ + +import typing + +from sqlalchemy import select, text +from sqlalchemy.ext.asyncio import AsyncSession + +from src.db.models.budget_line import BudgetLine + + +async def update_budget_line( + db: AsyncSession, + line_id: int, + table_name: str, + field_name: str, + value: typing.Any, + quarter_field: str | None = None, + quarter: int | None = None, +) -> typing.Any: + where_text = f"line_id = :line_id" + if quarter and quarter_field: + where_text = f"{where_text} AND {quarter_field} = :quarter" + + query_text = f"UPDATE {table_name} SET {field_name} = :value WHERE {where_text} RETURNING *" + query = text(query_text) + result = ( + await db.execute( + query, + {"line_id": line_id, "value": value, "quarter": quarter} + ) + ).fetchone() + return tuple(result) if result else None + + +async def get_budget_line( + db: AsyncSession, + id: int | None = None, + form_id: int | None = None, + expense_item_id: int | None = None, +) -> BudgetLine | None: + assert id or (expense_item_id and form_id) + where_clause = [] + if id: + where_clause.append(BudgetLine.id == id) + if expense_item_id: + where_clause.append(BudgetLine.expense_item_id == expense_item_id) + where_clause.append(BudgetLine.budget_form_id == form_id) + result = await db.execute(select(BudgetLine).where(*where_clause)) + return result.scalar_one_or_none() + + +async def create_budget_line( + db: AsyncSession, + form_id: int, + expense_item_id: int | None = None, +) -> BudgetLine: + result = BudgetLine( + expense_item_id=expense_item_id, + budget_form_id=form_id, + # internal_order: Mapped[str | None] = mapped_column(String, nullable=True) + name="Test" + ) + db.add( + result + ) + await db.flush() + return result diff --git a/api/src/data/expense_item.py b/api/src/data/expense_item.py new file mode 100644 index 0000000..7f5f4e5 --- /dev/null +++ b/api/src/data/expense_item.py @@ -0,0 +1,17 @@ +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from src.db.models import ExpenseItem +from src.db.models.budget_line import BudgetLine + + +async def get_expense_item_by_budget_line( + db: AsyncSession, + budget_line_id: int, +) -> ExpenseItem | None: + result = await db.execute( + select(ExpenseItem) + .join(BudgetLine, BudgetLine.expense_item_id == ExpenseItem.id) + .where(BudgetLine.id == budget_line_id) + ) + return result.scalar_one_or_none() diff --git a/api/src/data/page.py b/api/src/data/page.py new file mode 100644 index 0000000..7d9514a --- /dev/null +++ b/api/src/data/page.py @@ -0,0 +1,45 @@ + +from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncSession + + +async def get_page_with_counts( + db: AsyncSession, + form_id: int, + page: str, + offset: int | None = None, + limit: int | None = None, +) -> list: + query_text = "SELECT COUNT(*) OVER(), * FROM v_form1_sheet_jsonb(:form_id, :page)" + if offset is not None: + query_text += " OFFSET :offset" + if limit is not None: + query_text += " LIMIT :limit" + + query = text(query_text) + + return ( + await db.execute( + query, + {"form_id": form_id, "page": page, "limit": limit, "offset": offset} + ) + ).fetchall() + + + +async def get_page_row( + db: AsyncSession, + form_id: int, + page: str, + budget_line_id: int +) -> list: + query_text = "SELECT * FROM v_form1_sheet_jsonb(:form_id, :page) as res WHERE line_id = :line_id" + + query = text(query_text) + + return ( + await db.execute( + query, + {"form_id": form_id, "page": page, "line_id": budget_line_id} + ) + ).fetchone() diff --git a/api/src/db/__init__.py b/api/src/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/src/db/base.py b/api/src/db/base.py new file mode 100644 index 0000000..ab30fc4 --- /dev/null +++ b/api/src/db/base.py @@ -0,0 +1,42 @@ +from sqlalchemy.ext.asyncio import (AsyncAttrs, async_sessionmaker, + create_async_engine) +from sqlalchemy.orm import DeclarativeBase + +from src.conf.settings import get_settings + + +settings = get_settings() + +# Создание движка базы данных +connect_args = {} +# if settings.DATABASE_SCHEMA: +# connect_args["server_settings"] = { +# "search_path": settings.DATABASE_SCHEMA, +# } + +if "sqlite" in settings.DATABASE_URL: + connect_args["check_same_thread"] = False +engine = create_async_engine( # create_engine( + settings.DATABASE_URL, + pool_pre_ping=True, + pool_size=5, + max_overflow=10, + # echo=settings.DEBUG, + connect_args=connect_args, + # echo=True, +) + +# Создание фабрики сессий +# SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +SessionLocal = async_sessionmaker( + # autocommit=False, + autoflush=False, + bind=engine, + expire_on_commit=False, + # class_=AsyncSession +) + + +# Базовый класс для моделей +class Base(AsyncAttrs, DeclarativeBase): + pass diff --git a/api/src/db/models/__init__.py b/api/src/db/models/__init__.py new file mode 100644 index 0000000..84b8a17 --- /dev/null +++ b/api/src/db/models/__init__.py @@ -0,0 +1,5 @@ +from src.db.models.budget_form import BudgetForm +from src.db.models.budget_line import BudgetLine +from src.db.models.expense_item import ExpenseItem + +__all__ = ["BudgetForm", "BudgetLine", "ExpenseItem"] diff --git a/api/src/db/models/budget_form.py b/api/src/db/models/budget_form.py new file mode 100644 index 0000000..1fea268 --- /dev/null +++ b/api/src/db/models/budget_form.py @@ -0,0 +1,24 @@ +import typing + +from sqlalchemy import DateTime, Integer, String, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from src.db.base import Base + + +class BudgetForm(Base): + __tablename__ = "budget_form" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + form_type_code: Mapped[str] = mapped_column(String, nullable=False) + year: Mapped[typing.Optional[int]] = mapped_column(Integer, nullable=True) + ssp_id: Mapped[typing.Optional[str]] = mapped_column(String, nullable=True) + branch_id: Mapped[typing.Optional[int]] = mapped_column(Integer, nullable=True) + created_by: Mapped[typing.Optional[str]] = mapped_column(String, nullable=True) + created_at: Mapped[typing.Optional[str]] = mapped_column(DateTime, server_default=func.now(), nullable=True) + updated_by: Mapped[typing.Optional[str]] = mapped_column(String, nullable=True) + updated_at: Mapped[typing.Optional[str]] = mapped_column(DateTime, server_default=func.now(), nullable=True) + + budget_lines: Mapped[list["BudgetLine"]] = relationship( # type: ignore + "BudgetLine", back_populates="budget_form" + ) diff --git a/api/src/db/models/budget_line.py b/api/src/db/models/budget_line.py new file mode 100644 index 0000000..a1968fa --- /dev/null +++ b/api/src/db/models/budget_line.py @@ -0,0 +1,19 @@ +from sqlalchemy import ForeignKey, Integer, String +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from src.db.models.budget_form import BudgetForm +from src.db.base import Base + + +class BudgetLine(Base): + __tablename__ = "budget_line" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + expense_item_id: Mapped[int] = mapped_column(ForeignKey("expense_item.id")) + budget_form_id: Mapped[int] = mapped_column(ForeignKey("budget_form.id")) + internal_order: Mapped[str | None] = mapped_column(String, nullable=True) + name: Mapped[str | None] = mapped_column(String, nullable=True) + + budget_form: Mapped["BudgetForm | None"] = relationship( + "BudgetForm", back_populates="budget_lines" + ) diff --git a/api/src/db/models/expense_item.py b/api/src/db/models/expense_item.py new file mode 100644 index 0000000..d691053 --- /dev/null +++ b/api/src/db/models/expense_item.py @@ -0,0 +1,40 @@ +import typing + +from sqlalchemy import CheckConstraint, ForeignKey, Index, Integer, String +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from src.db.base import Base + + +class ExpenseItem(Base): + __tablename__ = "expense_item" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + section_code: Mapped[typing.Optional[str]] + item_id: Mapped[typing.Optional[str]] + num_group_id: Mapped[typing.Optional[str]] + name: Mapped[typing.Optional[str]] + sheet: Mapped[typing.Optional[str]] + direction: Mapped[typing.Optional[str]] + parent_id: Mapped[typing.Optional[int]] = mapped_column(ForeignKey("expense_item.id"), nullable=True) + depth: Mapped[typing.Optional[int]] = mapped_column(Integer, default=0, nullable=True) + + parent: Mapped["ExpenseItem | None"] = relationship( + "ExpenseItem", remote_side="ExpenseItem.id", back_populates="children" + ) + children: Mapped[list["ExpenseItem"]] = relationship( + "ExpenseItem", back_populates="parent" + ) + + # __table_args__ = ( + # CheckConstraint( + # "direction::text = ANY (ARRAY['Support'::character varying, 'Development'::character varying]::text[])", + # name="expense_item_direction_check", + # ), + # CheckConstraint( + # "sheet::text = ANY (ARRAY['AHR'::character varying, 'CAP'::character varying, 'OPER'::character varying]::text[])", + # name="expense_item_sheet_check", + # ), + # Index("ix_expense_item_parent", "parent_id"), + # Index("ix_expense_item_sheet", "sheet"), + # ) diff --git a/api/src/db/session.py b/api/src/db/session.py new file mode 100644 index 0000000..ffee502 --- /dev/null +++ b/api/src/db/session.py @@ -0,0 +1,26 @@ +from typing import AsyncGenerator + +from src.db.base import SessionLocal + + +async def get_db() -> AsyncGenerator: + """Dependency для получения сессии базы данных.""" + db = SessionLocal() + try: + yield db + await db.commit() + finally: + await db.close() + + +# async def run_migrations(): +# alembic_cfg = Config("alembic.ini") +# await asyncio.to_thread(command.upgrade, alembic_cfg, "head") + + +# async def create_tables(): +# """Создание таблиц в базе данных.""" +# # Исключаем устаревшую таблицу table_data из автосоздания, +# # чтобы она не пересоздавалась после миграций +# if not settings.DEBUG: +# await run_migrations() diff --git a/api/src/exceptions.py b/api/src/exceptions.py new file mode 100644 index 0000000..2862ca9 --- /dev/null +++ b/api/src/exceptions.py @@ -0,0 +1,21 @@ +import dataclasses + + +class AppError(Exception): + message: str = "Ошибка" + + def __init__(self, message: str | None = None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.message = message or self.message + + +class PermissionDenied(AppError): + message: str = "Нет доступа" + + +class NotFound(AppError): + message: str = "Не найдено" + + +class ValidationError(AppError): + message: str = "Ошибка валидации" diff --git a/api/src/routes/api/form.py b/api/src/routes/api/form.py new file mode 100644 index 0000000..c6e93b7 --- /dev/null +++ b/api/src/routes/api/form.py @@ -0,0 +1,73 @@ + + + +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from src.routes.api.schemas.base import BaseApiResponse +from src.routes.api.schemas.budget_line import CreateBudgetLineLSchema, UpdateLinkedBLStructureSchema +from src.routes.api.schemas import ListApiResponse +from src.services import page as page_service, budget_line as bl_service +from src.db.session import get_db + + + +router = APIRouter(prefix="/forms", tags=["forms"]) + + +@router.get("/{form_id}/{page}") +async def get_form_page( + form_id: int, + page: str, + db: AsyncSession = Depends(get_db), + offset: int | None = None, + limit: int | None = None, +) -> ListApiResponse[list]: + count, result = await page_service.get_page( + db=db, + form_id=form_id, + page=page, + offset=offset, + limit=limit, + ) + return ListApiResponse( + data=result, + count=count, + ) + + +@router.patch("/{form_id}/{page}/{line_id}") +async def update_form( + form_id: int, + page: str, + line_id: int, + field_info: UpdateLinkedBLStructureSchema, + db: AsyncSession = Depends(get_db), +): + return await bl_service.update_budget_line( + db=db, + model_name=field_info.model_name, + form_id=form_id, + line_id=line_id, + field_name=field_info.field_name, + value=field_info.value, + quarter=field_info.quarter, + ) + + + +@router.post("/{form_id}/{page}/") +async def insert_line( + form_id: int, + page: str, + budget_line: CreateBudgetLineLSchema, + db: AsyncSession = Depends(get_db), +) -> BaseApiResponse[tuple]: + return BaseApiResponse( + data=await bl_service.create_budget_line( + db=db, + form_id=form_id, + page=page, + expense_item_id=budget_line.expense_item_id, + ) + ) diff --git a/api/src/routes/api/route.py b/api/src/routes/api/route.py index cb26bef..d2db709 100644 --- a/api/src/routes/api/route.py +++ b/api/src/routes/api/route.py @@ -1,6 +1,8 @@ from fastapi import APIRouter from datetime import datetime, timezone +from src.routes.api import form + from .schemas import ApiResponse route = APIRouter(prefix="/api",tags=['Base']) @@ -14,4 +16,6 @@ async def get_data(): return ApiResponse( date_with_time=datetime.now(timezone.utc), text="Fast API приложение" - ) \ No newline at end of file + ) + +route.include_router(form.router) diff --git a/api/src/routes/api/schemas.py b/api/src/routes/api/schemas.py deleted file mode 100644 index 0a12428..0000000 --- a/api/src/routes/api/schemas.py +++ /dev/null @@ -1,7 +0,0 @@ -from pydantic import BaseModel -from datetime import datetime - - -class ApiResponse(BaseModel): - date_with_time: datetime - text: str \ No newline at end of file diff --git a/api/src/routes/api/schemas/__init__.py b/api/src/routes/api/schemas/__init__.py new file mode 100644 index 0000000..3b869e6 --- /dev/null +++ b/api/src/routes/api/schemas/__init__.py @@ -0,0 +1 @@ +from src.routes.api.schemas.base import * diff --git a/api/src/routes/api/schemas/base.py b/api/src/routes/api/schemas/base.py new file mode 100644 index 0000000..ef81dc9 --- /dev/null +++ b/api/src/routes/api/schemas/base.py @@ -0,0 +1,22 @@ +from typing import Generic, TypeVar + +from pydantic import BaseModel, Field +from datetime import datetime + +T = TypeVar("T") + + +class ApiResponse(BaseModel): + date_with_time: datetime + text: str + + +class ListApiResponse(BaseModel, Generic[T]): + data: list[T] + count: int + errors: list = Field(default_factory=list) + + +class BaseApiResponse(BaseModel, Generic[T]): + data: T + errors: list = Field(default_factory=list) diff --git a/api/src/routes/api/schemas/budget_line.py b/api/src/routes/api/schemas/budget_line.py new file mode 100644 index 0000000..1496d75 --- /dev/null +++ b/api/src/routes/api/schemas/budget_line.py @@ -0,0 +1,40 @@ + + +import enum +import typing + +from pydantic import BaseModel, Field + + +class LinkedBLStructureEnum(str, enum.Enum): + EXPENSE_ITEM = "expense_item" + PLAN = "plan" + CONTRACT_SUMMARY = "contract_summary" + ALLOCATION = "allocation" + SEQUESTRATION = "sequestration" + RESERVE = "reserve" + COLLEGIAL_APPROVAL = "collegial_approval" + CKK = "ckk" + CONTRACT_DETAIL = "contract_detail" + BUDGET_LINE_QUARTER = "budget_line_quarter" + + + + +class UpdateLinkedBLStructureSchema(BaseModel): + model_name: LinkedBLStructureEnum + field_name: str + value: typing.Any + quarter: typing.Optional[int] = Field(default=None, ge=1, le=4) + + +class CreateBudgetLineLSchema(BaseModel): + expense_item_id: int + + +# class BudgetLineReadSchema(BaseModel): +# id: int +# expense_item_id: int = mapped_column(ForeignKey("expense_item.id")) +# budget_form_id: Mapped[int] = mapped_column(ForeignKey("budget_form.id")) +# internal_order: Mapped[str | None] = mapped_column(String, nullable=True) +# name: Mapped[str | None] = mapped_column(String, nullable=True) \ No newline at end of file diff --git a/api/src/services/__init__.py b/api/src/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/src/services/budget_line.py b/api/src/services/budget_line.py new file mode 100644 index 0000000..91a45cd --- /dev/null +++ b/api/src/services/budget_line.py @@ -0,0 +1,187 @@ +import typing + +from sqlalchemy.ext.asyncio import AsyncSession + +from src.db.models.budget_line import BudgetLine +from src.exceptions import NotFound, PermissionDenied, ValidationError +from src.routes.api.schemas.budget_line import LinkedBLStructureEnum +from src.data import budget_line as bl_repo, page as page_repo + + +MODEL_FIELDS_TO_DB_MAPPING = { + + LinkedBLStructureEnum.EXPENSE_ITEM: { + "name": "name", + "item_id": "item_id", + "section": "section_code", + "num_group": "num_group_id", + }, + LinkedBLStructureEnum.PLAN: { + "q1": "plan_q1", + "q2": "plan_q2", + "q3": "plan_q3", + "q4": "plan_q4", + "comment": "comment" + }, + LinkedBLStructureEnum.CONTRACT_SUMMARY: { + "total": "total_amount", + "comment": "comment", + "deadline": "deadline", + "future_y1": "future_payments_y1", + "future_y2": "future_payments_y2", + "other_ssp": "other_ssp_amount", + "counterparty": "counterparty" + }, + LinkedBLStructureEnum.ALLOCATION: { + "order": "internal_order", + "property": "property_object", + }, + LinkedBLStructureEnum.SEQUESTRATION: { + "q1": "adj_q1", + "q2": "adj_q2", + "q3": "adj_q3", + "q4": "adj_q4", + "justification": "justification" + }, + LinkedBLStructureEnum.RESERVE: { + "q1": "amount_q1", + "q2": "amount_q2", + "q3": "amount_q3", + "q4": "amount_q4", + "justification": "justification" + }, + LinkedBLStructureEnum.COLLEGIAL_APPROVAL: { + "note": "note", + "approved": "approved_amount", + "protocol": "protocol_reference", + }, + LinkedBLStructureEnum.CKK: { + "q1": "expenses_q1", + "q2": "expenses_q2", + "q3": "expenses_q3", + "q4": "expenses_q4", + "ceiling": "ceiling_amount", + "comment": "comment", + "deadline": "delivery_deadline", + "proc_plan": "procurement_plan", + "proc_method": "procurement_method", + "rf_schedule": "rf_schedule", + }, + LinkedBLStructureEnum.CONTRACT_DETAIL: { + "q1": "expenses_q1", + "q2": "expenses_q2", + "q3": "expenses_q3", + "q4": "expenses_q4", + "act": "act", + "addenda": "addenda", + "ceiling": "ceiling_amount", + "comment": "comment", + "subject": "subject", + "currency": "currency", + "deadline": "deadline", + "vat_rate": "vat_rate", + "reference": "reference", + "rf_schedule": "rf_schedule", + "counterparty": "counterparty", + "exchange_rate": "exchange_rate", + "amount_foreign": "amount_foreign", + "payment_scheme": "payment_scheme", + }, + LinkedBLStructureEnum.BUDGET_LINE_QUARTER: { + "adj_rf": "adj_rf", + "payment_ho": "payment_amount_ho", + "pay_rf": "payment_amount_rf", + "adj_ssp": "adj_ssp", + "pay_act": "payment_act", + "pay_date": "payment_date", + "actual_m1": "actual_m1", + "actual_m2": "actual_m2", + "actual_m3": "actual_m3", + "pay_amount": "payment_amount", + "adj_comment": "adj_comment", + "adj_current": "adj_current", + "adj_reserve": "adj_reserve", + "pay_comment": "payment_comment", + "transfer_q2": "transfer_to_q2", + "transfer_q3": "transfer_to_q3", + "transfer_q4": "transfer_to_q4", + }, + + +} + + +async def update_budget_line( + db: AsyncSession, + form_id: int, + line_id: int, + model_name: LinkedBLStructureEnum, + field_name: str, + value: typing.Any, + quarter: int | None = None, +) -> typing.Any: + quarter_field = None + + line = await bl_repo.get_budget_line( + db=db, + id=line_id, + ) + if not line or line.budget_form_id != form_id: + raise NotFound + + # нужно рефрешить матвью и обновлять name у budget_line + # case LinkedBLStructureEnum.EXPENSE_ITEM: + # expense_item = await expense_item_repo.get_expense_item_by_budget_line( + # db=db, + # budget_line_id=line_id, + # ) + # if expense_item.depth != 3: + # raise PermissionDenied(message="У вас нет прав на редактирование этой строки") + # setattr( + # expense_item, + # MODEL_FIELDS_TO_DB_MAPPING[model_name][field_name], + # value, + # ) + # await db.flush() + if model_name == LinkedBLStructureEnum.BUDGET_LINE_QUARTER: + quarter_field = "quarter" + + db_field = MODEL_FIELDS_TO_DB_MAPPING[model_name].get(field_name) + return await bl_repo.update_budget_line( + db=db, + table_name=model_name.value, + line_id=line_id, + field_name=db_field, + value=value, + quarter_field=quarter_field, + quarter=quarter, + ) + + +async def create_budget_line( + db: AsyncSession, + form_id: int, + page: str, + # line_after_id: int, + expense_item_id: int, +) -> tuple: + + line = await bl_repo.get_budget_line( + db=db, + form_id=form_id, + expense_item_id=expense_item_id, + ) + if not line: + raise NotFound + budget_line = await bl_repo.create_budget_line( + db=db, + form_id=form_id, + expense_item_id=expense_item_id + ) + result = await page_repo.get_page_row( + db=db, + form_id=form_id, + page=page, + budget_line_id=budget_line.id, + ) + return tuple(result) if result else None diff --git a/api/src/services/page.py b/api/src/services/page.py new file mode 100644 index 0000000..59e496b --- /dev/null +++ b/api/src/services/page.py @@ -0,0 +1,53 @@ + + +from sqlalchemy.ext.asyncio import AsyncSession + +from src.data.page import get_page_with_counts + + + +async def get_page( + db: AsyncSession, + form_id: int, + page: str, + offset: int | None = None, + limit: int | None = None, +) -> tuple[int, list[tuple]]: + pre_result = await get_page_with_counts( + db=db, + form_id=form_id, + page=page, + offset=offset, + limit=limit, + ) + + if not pre_result: + return 0, pre_result + count = pre_result[0][0] + return count, [tuple(x[1:]) for x in pre_result] + + + +async def update_line( + db: AsyncSession, + line_id: int, +): + pass + # query_text = "SELECT COUNT(*) OVER(), * FROM v_form1_sheet_jsonb(:form_id, :page)" + # if offset is not None: + # query_text += " OFFSET :offset" + # if limit is not None: + # query_text += " LIMIT :limit" + + # query = text(query_text) + + # result = ( + # await db.execute( + # query, + # {"form_id": form_id, "page": page, "limit": limit, "offset": offset} + # ) + # ).fetchall() + # if not result: + # return 0, result + # count = result[0][0] + # return count, [tuple(x[1:]) for x in result]