feat(backend): add FastAPI backend — procurement, inventory domain, Casdoor auth, PM arrival endpoints
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
"""Schedule steps nested under termsheets. step_no auto-numbers (MAX+1)."""
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.engine import get_db
|
||||
from app.models.procurement import Termsheet, TermsheetScheduleStep
|
||||
from app.schemas.procurement import ScheduleStepCreate, ScheduleStepUpdate
|
||||
from app.services.expand_service import schedule_step_to_dict
|
||||
from app.services.quote_service import next_step_no
|
||||
from app.utils.errors import ConflictError, NotFoundError
|
||||
from app.utils.pagination import paginate
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/quotes/{quote_id}/termsheets/{termsheet_id}/schedule-steps",
|
||||
tags=["schedule-steps"],
|
||||
)
|
||||
|
||||
|
||||
async def _termsheet_or_404(db: AsyncSession, quote_id, termsheet_id) -> Termsheet:
|
||||
ts = await db.scalar(
|
||||
select(Termsheet).where(Termsheet.id == termsheet_id, Termsheet.quote_id == quote_id)
|
||||
)
|
||||
if ts is None:
|
||||
raise NotFoundError("ترمشیت یافت نشد")
|
||||
return ts
|
||||
|
||||
|
||||
async def _step_or_404(db: AsyncSession, termsheet_id, step_id) -> TermsheetScheduleStep:
|
||||
step = await db.scalar(
|
||||
select(TermsheetScheduleStep).where(
|
||||
TermsheetScheduleStep.id == step_id, TermsheetScheduleStep.termsheet_id == termsheet_id
|
||||
)
|
||||
)
|
||||
if step is None:
|
||||
raise NotFoundError("مرحله پرداخت یافت نشد")
|
||||
return step
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_schedule_steps(
|
||||
quote_id,
|
||||
termsheet_id,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
await _termsheet_or_404(db, quote_id, termsheet_id)
|
||||
stmt = (
|
||||
select(TermsheetScheduleStep)
|
||||
.where(TermsheetScheduleStep.termsheet_id == termsheet_id)
|
||||
.order_by(TermsheetScheduleStep.step_no.asc())
|
||||
)
|
||||
items, meta = await paginate(stmt, page, page_size, db)
|
||||
return {"data": [schedule_step_to_dict(s) for s in items], "meta": meta}
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
async def create_schedule_step(
|
||||
quote_id, termsheet_id, payload: ScheduleStepCreate, db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
ts = await _termsheet_or_404(db, quote_id, termsheet_id)
|
||||
step_no = payload.step_no
|
||||
if step_no is None:
|
||||
step_no = await next_step_no(db, ts.id)
|
||||
duplicate = await db.scalar(
|
||||
select(TermsheetScheduleStep.id).where(
|
||||
TermsheetScheduleStep.termsheet_id == ts.id, TermsheetScheduleStep.step_no == step_no
|
||||
)
|
||||
)
|
||||
if duplicate is not None:
|
||||
raise ConflictError("شماره مرحله تکراری است", field="step_no")
|
||||
step = TermsheetScheduleStep(
|
||||
termsheet_id=ts.id,
|
||||
step_no=step_no,
|
||||
due_date=payload.due_date,
|
||||
amount=payload.amount,
|
||||
note=payload.note,
|
||||
)
|
||||
db.add(step)
|
||||
await db.flush()
|
||||
return {"data": schedule_step_to_dict(step)}
|
||||
|
||||
|
||||
@router.patch("/{step_id}")
|
||||
async def update_schedule_step(
|
||||
quote_id, termsheet_id, step_id, payload: ScheduleStepUpdate, db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
step = await _step_or_404(db, termsheet_id, step_id)
|
||||
if payload.step_no is not None and payload.step_no != step.step_no:
|
||||
duplicate = await db.scalar(
|
||||
select(TermsheetScheduleStep.id).where(
|
||||
TermsheetScheduleStep.termsheet_id == termsheet_id,
|
||||
TermsheetScheduleStep.step_no == payload.step_no,
|
||||
TermsheetScheduleStep.id != step_id,
|
||||
)
|
||||
)
|
||||
if duplicate is not None:
|
||||
raise ConflictError("شماره مرحله تکراری است", field="step_no")
|
||||
step.step_no = payload.step_no
|
||||
if payload.due_date is not None:
|
||||
step.due_date = payload.due_date
|
||||
if payload.amount is not None:
|
||||
step.amount = payload.amount
|
||||
if payload.note is not None:
|
||||
step.note = payload.note
|
||||
await db.flush()
|
||||
return {"data": schedule_step_to_dict(step)}
|
||||
|
||||
|
||||
@router.delete("/{step_id}")
|
||||
async def delete_schedule_step(quote_id, termsheet_id, step_id, db: AsyncSession = Depends(get_db)):
|
||||
step = await _step_or_404(db, termsheet_id, step_id)
|
||||
count = await db.scalar(
|
||||
select(func.count()).select_from(TermsheetScheduleStep).where(
|
||||
TermsheetScheduleStep.termsheet_id == termsheet_id
|
||||
)
|
||||
)
|
||||
if count <= 1:
|
||||
raise ConflictError("آخرین مرحله پرداخت قابل حذف نیست")
|
||||
await db.delete(step)
|
||||
await db.flush()
|
||||
return {"data": {"deleted": True}}
|
||||
Reference in New Issue
Block a user