Files
ERP/backend/app/routers/schedule_steps.py
T
MMD 0ea0322967 Revert 253c922: restore the PM/RBAC work to the integration
Reverting the production revert so the pm branch can merge cleanly on
top of the restored RBAC base. Conflicts against newer integration work
resolved by combining:
- App.tsx: v7's bot-draft wizard bypass + login returnUrl kept alongside
  the supervisor gating (no-project block screen, panel scoping)
- LoginPage: returnUrl navigation kept, supervisor routing added
- migration.sql: bot_drafts table kept, unified-projects ALTERs restored
2026-10-01 15:24:56 +03:30

141 lines
4.7 KiB
Python

"""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.auth import require_role
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"],
)
_procurement_writer = require_role("admin", "procurement_manager")
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),
_auth=Depends(_procurement_writer),
):
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),
_auth=Depends(_procurement_writer),
):
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),
_auth=Depends(_procurement_writer),
):
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}}