"""运输计划管理 - 运费结算 API""" from datetime import date from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session from app.core.database import get_db from app.core.db_auth import DbUserContext, get_db_current_user from app.core.tenant_context import get_tenant_no from app.models.pl_models import PlFreightSettlement from app.schemas.pl.freight import ( FreightSettlementApprovalActionBody, FreightSettlementByTransportRead, FreightSettlementCreate, FreightSettlementDisburseBody, FreightSettlementListResponse, FreightSettlementRead, FreightSettlementSubmitApprovalBody, FreightSettlementUpdate, FreightSettlementWithTransports, RemittanceVoucherRead, UnsettledPlanListResponse, ) from app.services import pl_freight_settlement_service as freight_svc router = APIRouter(prefix="/freight", tags=["运输计划-运费结算"]) def _generate_settlement_no(db: Session) -> str: today = date.today() prefix = f"YF{today.strftime('%Y%m%d')}" last = ( db.query(PlFreightSettlement) .filter( PlFreightSettlement.settlement_no.like(f"{prefix}%"), PlFreightSettlement.del_flag == 0, ) .order_by(PlFreightSettlement.settlement_no.desc()) .first() ) seq = int(last.settlement_no[-2:]) + 1 if last else 1 return f"{prefix}{seq:02d}" @router.get("/unsettled-plans", response_model=UnsettledPlanListResponse) def list_unsettled_plans( q: Optional[str] = Query(default=None), carrier_id: Optional[str] = Query(default=None, alias="carrierId"), plan_type: Optional[str] = Query(default=None, alias="planType"), license_plate: Optional[str] = Query(default=None, alias="licensePlate"), page: int = Query(default=1, ge=1), page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"), db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> UnsettledPlanListResponse: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) total, items = freight_svc.list_unsettled_plans( db, tenant_no=tenant_no, user=user, q=q, carrier_id=carrier_id, plan_type=plan_type, license_plate=license_plate, page=page, page_size=page_size, ) return UnsettledPlanListResponse(total=total, list=items) @router.get("/", response_model=FreightSettlementListResponse) def list_settlements( q: Optional[str] = Query(default=None), carrier_id: Optional[str] = Query(default=None, alias="carrierId"), settlement_status: Optional[str] = Query(default=None, alias="settlementStatus"), tab: Optional[str] = Query( default=None, description="bills|approval_pending|approval_done|disburse|settled", ), page: int = Query(default=1, ge=1), page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"), db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> FreightSettlementListResponse: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) total, items = freight_svc.list_settlements_enriched( db, tenant_no=tenant_no, user_id=user.id, user=user, q=q, carrier_id=carrier_id, settlement_status=settlement_status, tab=tab, page=page, page_size=page_size, ) return FreightSettlementListResponse(total=total, list=items) @router.get("/by-transport/{transport_id}", response_model=Optional[FreightSettlementByTransportRead]) def get_settlement_by_transport( transport_id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> Optional[FreightSettlementByTransportRead]: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) detail = freight_svc.get_settlement_by_transport_id( db, tenant_no=tenant_no, transport_id=transport_id, user=user ) if not detail: return None return FreightSettlementByTransportRead.model_validate(detail) @router.post("/", response_model=FreightSettlementRead, status_code=status.HTTP_201_CREATED) def create_settlement( payload: FreightSettlementCreate, db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> FreightSettlementRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) settlement = freight_svc.create_settlement( db, tenant_no=tenant_no, plan_ids=payload.plan_ids, transport_ids=payload.transport_ids, user_id=user.id, dept_id=user.dept_id, settlement_no=_generate_settlement_no(db), ) return settlement @router.get("/{id}", response_model=FreightSettlementWithTransports) def get_settlement( id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> FreightSettlementWithTransports: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) detail = freight_svc.get_settlement_detail( db, tenant_no=tenant_no, settlement_id=id, user_id=user.id, user=user ) return FreightSettlementWithTransports.model_validate(detail) @router.put("/{id}", response_model=FreightSettlementRead) def update_settlement( id: str, payload: FreightSettlementUpdate, db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> FreightSettlementRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) return freight_svc.update_settlement_draft( db, tenant_no=tenant_no, settlement_id=id, payload=payload, user_id=user.id, user=user, ) @router.post("/{id}/cancel", response_model=FreightSettlementRead) def cancel_settlement( id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> FreightSettlementRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) return freight_svc.cancel_draft_settlement( db, tenant_no=tenant_no, settlement_id=id, user_id=user.id, user=user ) @router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT) def delete_settlement( id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> None: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) freight_svc.delete_draft_settlement( db, tenant_no=tenant_no, settlement_id=id, user_id=user.id, user=user ) @router.post("/{id}/submit-approval", response_model=FreightSettlementRead) def submit_settlement_approval( id: str, payload: FreightSettlementSubmitApprovalBody, db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> FreightSettlementRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) return freight_svc.submit_settlement_approval( db, tenant_no=tenant_no, settlement_id=id, body=payload, user_id=user.id, dept_id=user.dept_id, user=user, ) @router.post("/approval/{approval_id}", response_model=FreightSettlementRead) def process_settlement_approval( approval_id: str, payload: FreightSettlementApprovalActionBody, db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> FreightSettlementRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) if payload.action not in ("approve", "reject"): raise HTTPException(status_code=400, detail="action 必须为 approve 或 reject") return freight_svc.process_settlement_approval( db, tenant_no=tenant_no, approval_id=approval_id, action=payload.action, comment=payload.comment, user_id=user.id, ) @router.post("/{id}/withdraw-approval", response_model=FreightSettlementRead) def withdraw_settlement_approval( id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> FreightSettlementRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) return freight_svc.withdraw_settlement_approval( db, tenant_no=tenant_no, settlement_id=id, user_id=user.id, user=user ) @router.get("/{id}/approval-flow") def get_settlement_approval_flow( id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ): tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) freight_svc.get_settlement_row(db, tenant_no, id, user=user) return freight_svc.get_approval_flow(db, id) @router.post("/{id}/disburse", response_model=FreightSettlementRead) def disburse_settlement( id: str, payload: FreightSettlementDisburseBody, db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> FreightSettlementRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) return freight_svc.disburse_settlement( db, tenant_no=tenant_no, settlement_id=id, body=payload, user_id=user.id, dept_id=user.dept_id, user=user, ) # 向后兼容旧接口 @router.post("/{id}/submit", response_model=FreightSettlementRead, deprecated=True) def submit_settlement_legacy( id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> FreightSettlementRead: raise HTTPException(status_code=400, detail="请使用 submit-approval 接口并选择审批人") @router.post("/{id}/approve", response_model=FreightSettlementRead, deprecated=True) def approve_settlement_legacy( id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> FreightSettlementRead: raise HTTPException(status_code=400, detail="请使用 approval/{approvalId} 接口审批") @router.get("/voucher/{settlement_id}", response_model=list[RemittanceVoucherRead]) def list_vouchers( settlement_id: str, db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> list[RemittanceVoucherRead]: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) freight_svc.get_settlement_row(db, tenant_no, settlement_id, user=user) detail = freight_svc.get_settlement_detail( db, tenant_no=tenant_no, settlement_id=settlement_id, user_id=user.id, user=user ) return detail.get("vouchers") or []