freight.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. """运输计划管理 - 运费结算 API"""
  2. from datetime import date
  3. from typing import Optional
  4. from fastapi import APIRouter, Depends, HTTPException, Query, status
  5. from sqlalchemy.orm import Session
  6. from app.core.database import get_db
  7. from app.core.db_auth import DbUserContext, get_db_current_user
  8. from app.core.tenant_context import get_tenant_no
  9. from app.models.pl_models import PlFreightSettlement
  10. from app.schemas.pl.freight import (
  11. FreightSettlementApprovalActionBody,
  12. FreightSettlementByTransportRead,
  13. FreightSettlementCreate,
  14. FreightSettlementDisburseBody,
  15. FreightSettlementListResponse,
  16. FreightSettlementRead,
  17. FreightSettlementSubmitApprovalBody,
  18. FreightSettlementUpdate,
  19. FreightSettlementWithTransports,
  20. RemittanceVoucherRead,
  21. UnsettledPlanListResponse,
  22. )
  23. from app.services import pl_freight_settlement_service as freight_svc
  24. router = APIRouter(prefix="/freight", tags=["运输计划-运费结算"])
  25. def _generate_settlement_no(db: Session) -> str:
  26. today = date.today()
  27. prefix = f"YF{today.strftime('%Y%m%d')}"
  28. last = (
  29. db.query(PlFreightSettlement)
  30. .filter(
  31. PlFreightSettlement.settlement_no.like(f"{prefix}%"),
  32. PlFreightSettlement.del_flag == 0,
  33. )
  34. .order_by(PlFreightSettlement.settlement_no.desc())
  35. .first()
  36. )
  37. seq = int(last.settlement_no[-2:]) + 1 if last else 1
  38. return f"{prefix}{seq:02d}"
  39. @router.get("/unsettled-plans", response_model=UnsettledPlanListResponse)
  40. def list_unsettled_plans(
  41. q: Optional[str] = Query(default=None),
  42. carrier_id: Optional[str] = Query(default=None, alias="carrierId"),
  43. plan_type: Optional[str] = Query(default=None, alias="planType"),
  44. license_plate: Optional[str] = Query(default=None, alias="licensePlate"),
  45. page: int = Query(default=1, ge=1),
  46. page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
  47. db: Session = Depends(get_db),
  48. user: DbUserContext = Depends(get_db_current_user),
  49. ) -> UnsettledPlanListResponse:
  50. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  51. total, items = freight_svc.list_unsettled_plans(
  52. db,
  53. tenant_no=tenant_no,
  54. user=user,
  55. q=q,
  56. carrier_id=carrier_id,
  57. plan_type=plan_type,
  58. license_plate=license_plate,
  59. page=page,
  60. page_size=page_size,
  61. )
  62. return UnsettledPlanListResponse(total=total, list=items)
  63. @router.get("/", response_model=FreightSettlementListResponse)
  64. def list_settlements(
  65. q: Optional[str] = Query(default=None),
  66. carrier_id: Optional[str] = Query(default=None, alias="carrierId"),
  67. settlement_status: Optional[str] = Query(default=None, alias="settlementStatus"),
  68. tab: Optional[str] = Query(
  69. default=None,
  70. description="bills|approval_pending|approval_done|disburse|settled",
  71. ),
  72. page: int = Query(default=1, ge=1),
  73. page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
  74. db: Session = Depends(get_db),
  75. user: DbUserContext = Depends(get_db_current_user),
  76. ) -> FreightSettlementListResponse:
  77. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  78. total, items = freight_svc.list_settlements_enriched(
  79. db,
  80. tenant_no=tenant_no,
  81. user_id=user.id,
  82. user=user,
  83. q=q,
  84. carrier_id=carrier_id,
  85. settlement_status=settlement_status,
  86. tab=tab,
  87. page=page,
  88. page_size=page_size,
  89. )
  90. return FreightSettlementListResponse(total=total, list=items)
  91. @router.get("/by-transport/{transport_id}", response_model=Optional[FreightSettlementByTransportRead])
  92. def get_settlement_by_transport(
  93. transport_id: str,
  94. db: Session = Depends(get_db),
  95. user: DbUserContext = Depends(get_db_current_user),
  96. ) -> Optional[FreightSettlementByTransportRead]:
  97. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  98. detail = freight_svc.get_settlement_by_transport_id(
  99. db, tenant_no=tenant_no, transport_id=transport_id, user=user
  100. )
  101. if not detail:
  102. return None
  103. return FreightSettlementByTransportRead.model_validate(detail)
  104. @router.post("/", response_model=FreightSettlementRead, status_code=status.HTTP_201_CREATED)
  105. def create_settlement(
  106. payload: FreightSettlementCreate,
  107. db: Session = Depends(get_db),
  108. user: DbUserContext = Depends(get_db_current_user),
  109. ) -> FreightSettlementRead:
  110. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  111. settlement = freight_svc.create_settlement(
  112. db,
  113. tenant_no=tenant_no,
  114. plan_ids=payload.plan_ids,
  115. transport_ids=payload.transport_ids,
  116. user_id=user.id,
  117. dept_id=user.dept_id,
  118. settlement_no=_generate_settlement_no(db),
  119. )
  120. return settlement
  121. @router.get("/{id}", response_model=FreightSettlementWithTransports)
  122. def get_settlement(
  123. id: str,
  124. db: Session = Depends(get_db),
  125. user: DbUserContext = Depends(get_db_current_user),
  126. ) -> FreightSettlementWithTransports:
  127. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  128. detail = freight_svc.get_settlement_detail(
  129. db, tenant_no=tenant_no, settlement_id=id, user_id=user.id, user=user
  130. )
  131. return FreightSettlementWithTransports.model_validate(detail)
  132. @router.put("/{id}", response_model=FreightSettlementRead)
  133. def update_settlement(
  134. id: str,
  135. payload: FreightSettlementUpdate,
  136. db: Session = Depends(get_db),
  137. user: DbUserContext = Depends(get_db_current_user),
  138. ) -> FreightSettlementRead:
  139. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  140. return freight_svc.update_settlement_draft(
  141. db,
  142. tenant_no=tenant_no,
  143. settlement_id=id,
  144. payload=payload,
  145. user_id=user.id,
  146. user=user,
  147. )
  148. @router.post("/{id}/cancel", response_model=FreightSettlementRead)
  149. def cancel_settlement(
  150. id: str,
  151. db: Session = Depends(get_db),
  152. user: DbUserContext = Depends(get_db_current_user),
  153. ) -> FreightSettlementRead:
  154. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  155. return freight_svc.cancel_draft_settlement(
  156. db, tenant_no=tenant_no, settlement_id=id, user_id=user.id, user=user
  157. )
  158. @router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
  159. def delete_settlement(
  160. id: str,
  161. db: Session = Depends(get_db),
  162. user: DbUserContext = Depends(get_db_current_user),
  163. ) -> None:
  164. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  165. freight_svc.delete_draft_settlement(
  166. db, tenant_no=tenant_no, settlement_id=id, user_id=user.id, user=user
  167. )
  168. @router.post("/{id}/submit-approval", response_model=FreightSettlementRead)
  169. def submit_settlement_approval(
  170. id: str,
  171. payload: FreightSettlementSubmitApprovalBody,
  172. db: Session = Depends(get_db),
  173. user: DbUserContext = Depends(get_db_current_user),
  174. ) -> FreightSettlementRead:
  175. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  176. return freight_svc.submit_settlement_approval(
  177. db,
  178. tenant_no=tenant_no,
  179. settlement_id=id,
  180. body=payload,
  181. user_id=user.id,
  182. dept_id=user.dept_id,
  183. user=user,
  184. )
  185. @router.post("/approval/{approval_id}", response_model=FreightSettlementRead)
  186. def process_settlement_approval(
  187. approval_id: str,
  188. payload: FreightSettlementApprovalActionBody,
  189. db: Session = Depends(get_db),
  190. user: DbUserContext = Depends(get_db_current_user),
  191. ) -> FreightSettlementRead:
  192. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  193. if payload.action not in ("approve", "reject"):
  194. raise HTTPException(status_code=400, detail="action 必须为 approve 或 reject")
  195. return freight_svc.process_settlement_approval(
  196. db,
  197. tenant_no=tenant_no,
  198. approval_id=approval_id,
  199. action=payload.action,
  200. comment=payload.comment,
  201. user_id=user.id,
  202. )
  203. @router.post("/{id}/withdraw-approval", response_model=FreightSettlementRead)
  204. def withdraw_settlement_approval(
  205. id: str,
  206. db: Session = Depends(get_db),
  207. user: DbUserContext = Depends(get_db_current_user),
  208. ) -> FreightSettlementRead:
  209. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  210. return freight_svc.withdraw_settlement_approval(
  211. db, tenant_no=tenant_no, settlement_id=id, user_id=user.id, user=user
  212. )
  213. @router.get("/{id}/approval-flow")
  214. def get_settlement_approval_flow(
  215. id: str,
  216. db: Session = Depends(get_db),
  217. user: DbUserContext = Depends(get_db_current_user),
  218. ):
  219. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  220. freight_svc.get_settlement_row(db, tenant_no, id, user=user)
  221. return freight_svc.get_approval_flow(db, id)
  222. @router.post("/{id}/disburse", response_model=FreightSettlementRead)
  223. def disburse_settlement(
  224. id: str,
  225. payload: FreightSettlementDisburseBody,
  226. db: Session = Depends(get_db),
  227. user: DbUserContext = Depends(get_db_current_user),
  228. ) -> FreightSettlementRead:
  229. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  230. return freight_svc.disburse_settlement(
  231. db,
  232. tenant_no=tenant_no,
  233. settlement_id=id,
  234. body=payload,
  235. user_id=user.id,
  236. dept_id=user.dept_id,
  237. user=user,
  238. )
  239. # 向后兼容旧接口
  240. @router.post("/{id}/submit", response_model=FreightSettlementRead, deprecated=True)
  241. def submit_settlement_legacy(
  242. id: str,
  243. db: Session = Depends(get_db),
  244. user: DbUserContext = Depends(get_db_current_user),
  245. ) -> FreightSettlementRead:
  246. raise HTTPException(status_code=400, detail="请使用 submit-approval 接口并选择审批人")
  247. @router.post("/{id}/approve", response_model=FreightSettlementRead, deprecated=True)
  248. def approve_settlement_legacy(
  249. id: str,
  250. db: Session = Depends(get_db),
  251. user: DbUserContext = Depends(get_db_current_user),
  252. ) -> FreightSettlementRead:
  253. raise HTTPException(status_code=400, detail="请使用 approval/{approvalId} 接口审批")
  254. @router.get("/voucher/{settlement_id}", response_model=list[RemittanceVoucherRead])
  255. def list_vouchers(
  256. settlement_id: str,
  257. db: Session = Depends(get_db),
  258. user: DbUserContext = Depends(get_db_current_user),
  259. ) -> list[RemittanceVoucherRead]:
  260. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  261. freight_svc.get_settlement_row(db, tenant_no, settlement_id, user=user)
  262. detail = freight_svc.get_settlement_detail(
  263. db, tenant_no=tenant_no, settlement_id=settlement_id, user_id=user.id, user=user
  264. )
  265. return detail.get("vouchers") or []