test-tsygankov
This commit is contained in:
parent
455fcd1240
commit
60939a2cf2
Binary file not shown.
@ -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
|
||||
|
||||
@ -10,6 +10,7 @@ from functools import lru_cache
|
||||
class Settings(BaseSettings):
|
||||
""" Настройки приложения """
|
||||
ROOT_PATH: str = '/'
|
||||
DATABASE_URL: str = ''
|
||||
|
||||
server: ServerSettings = ServerSettings()
|
||||
|
||||
|
||||
0
api/src/data/__init__.py
Normal file
0
api/src/data/__init__.py
Normal file
66
api/src/data/budget_line.py
Normal file
66
api/src/data/budget_line.py
Normal file
@ -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
|
||||
17
api/src/data/expense_item.py
Normal file
17
api/src/data/expense_item.py
Normal file
@ -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()
|
||||
45
api/src/data/page.py
Normal file
45
api/src/data/page.py
Normal file
@ -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()
|
||||
0
api/src/db/__init__.py
Normal file
0
api/src/db/__init__.py
Normal file
42
api/src/db/base.py
Normal file
42
api/src/db/base.py
Normal file
@ -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
|
||||
5
api/src/db/models/__init__.py
Normal file
5
api/src/db/models/__init__.py
Normal file
@ -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"]
|
||||
24
api/src/db/models/budget_form.py
Normal file
24
api/src/db/models/budget_form.py
Normal file
@ -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"
|
||||
)
|
||||
19
api/src/db/models/budget_line.py
Normal file
19
api/src/db/models/budget_line.py
Normal file
@ -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"
|
||||
)
|
||||
40
api/src/db/models/expense_item.py
Normal file
40
api/src/db/models/expense_item.py
Normal file
@ -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"),
|
||||
# )
|
||||
26
api/src/db/session.py
Normal file
26
api/src/db/session.py
Normal file
@ -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()
|
||||
21
api/src/exceptions.py
Normal file
21
api/src/exceptions.py
Normal file
@ -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 = "Ошибка валидации"
|
||||
73
api/src/routes/api/form.py
Normal file
73
api/src/routes/api/form.py
Normal file
@ -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,
|
||||
)
|
||||
)
|
||||
@ -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 приложение"
|
||||
)
|
||||
)
|
||||
|
||||
route.include_router(form.router)
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ApiResponse(BaseModel):
|
||||
date_with_time: datetime
|
||||
text: str
|
||||
1
api/src/routes/api/schemas/__init__.py
Normal file
1
api/src/routes/api/schemas/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from src.routes.api.schemas.base import *
|
||||
22
api/src/routes/api/schemas/base.py
Normal file
22
api/src/routes/api/schemas/base.py
Normal file
@ -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)
|
||||
40
api/src/routes/api/schemas/budget_line.py
Normal file
40
api/src/routes/api/schemas/budget_line.py
Normal file
@ -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)
|
||||
0
api/src/services/__init__.py
Normal file
0
api/src/services/__init__.py
Normal file
187
api/src/services/budget_line.py
Normal file
187
api/src/services/budget_line.py
Normal file
@ -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
|
||||
53
api/src/services/page.py
Normal file
53
api/src/services/page.py
Normal file
@ -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]
|
||||
Loading…
x
Reference in New Issue
Block a user