vehicle_maintenance.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. """车辆维保 API"""
  2. from datetime import date
  3. from datetime import datetime
  4. from typing import Any, List, Optional
  5. from pydantic import BaseModel, Field
  6. from fastapi import APIRouter, Body, Depends, HTTPException, Query, Request, status
  7. from sqlalchemy.orm import Session
  8. from app.core.database import get_db
  9. from app.core.db_auth import DbUserContext, get_db_current_user
  10. from app.core.tenant_context import get_tenant_no
  11. from app.schemas.pl.cancel_reason import CancelReasonBody
  12. from app.utils.excel_export import (
  13. EXPORT_MAX_ROWS,
  14. ExportColumn,
  15. ExportValidationError,
  16. export_rows_to_xlsx_response,
  17. filter_export_columns,
  18. )
  19. from app.schemas.pl.vehicle_maintenance import (
  20. VehicleMaintenanceApplyCreate,
  21. VehicleMaintenanceApplyListResponse,
  22. VehicleMaintenanceApplyRead,
  23. VehicleMaintenanceApplyReminder,
  24. VehicleMaintenanceApplyUpdate,
  25. VehicleMaintenanceProcessBoardListResponse,
  26. VehicleMaintenanceProcessCreate,
  27. VehicleMaintenanceProcessRead,
  28. VehicleMaintenanceProcessUpdate,
  29. )
  30. from app.services import pl_vehicle_maintenance_service as svc
  31. apply_router = APIRouter(prefix="/vehicle-maintenance-apply", tags=["运输计划-车辆维保申请"])
  32. process_router = APIRouter(prefix="/vehicle-maintenance-process", tags=["运输计划-车辆维保处理"])
  33. def _merge_query(request: Request, **known) -> dict:
  34. out = {k: v for k, v in known.items() if v is not None and v != ""}
  35. for k, v in request.query_params.multi_items():
  36. if k in ("page", "pageSize"):
  37. continue
  38. if k not in out and v not in (None, ""):
  39. out[k] = v
  40. return out
  41. def _param_get(params: dict, *keys: str) -> Any:
  42. for key in keys:
  43. if key in params and params[key] not in (None, ""):
  44. return params[key]
  45. return None
  46. def _body_to_params(body: BaseModel) -> dict:
  47. data = body.model_dump(by_alias=True, exclude_none=True)
  48. for key in ("ids", "columns"):
  49. data.pop(key, None)
  50. extra = getattr(body, "model_extra", None) or {}
  51. for key, value in extra.items():
  52. if key not in data and value not in (None, ""):
  53. data[key] = value
  54. return data
  55. def _apply_list_kwargs(params: dict) -> dict:
  56. return dict(
  57. q=_param_get(params, "q"),
  58. apply_no=_param_get(params, "applyNo", "apply_no"),
  59. license_plate=_param_get(params, "licensePlate", "license_plate"),
  60. maintenance_type=_param_get(params, "maintenanceType", "maintenance_type"),
  61. status=_param_get(params, "status"),
  62. status_in=_param_get(params, "statusIn", "status_in"),
  63. apply_date_from=_param_get(params, "applyDateFrom", "apply_date_from"),
  64. apply_date_to=_param_get(params, "applyDateTo", "apply_date_to"),
  65. start_date_from=_param_get(params, "startDateFrom", "start_date_from"),
  66. start_date_to=_param_get(params, "startDateTo", "start_date_to"),
  67. expected_pickup_date_from=_param_get(
  68. params, "expectedPickupDateFrom", "expected_pickup_date_from"
  69. ),
  70. expected_pickup_date_to=_param_get(
  71. params, "expectedPickupDateTo", "expected_pickup_date_to"
  72. ),
  73. applicant_id=_param_get(params, "applicantId", "applicant_id"),
  74. applicant_name=_param_get(params, "applicantName", "applicant_name"),
  75. create_time_from=_param_get(params, "createTimeFrom", "create_time_from"),
  76. create_time_to=_param_get(params, "createTimeTo", "create_time_to"),
  77. sort_field=_param_get(params, "sortField", "sort_field"),
  78. sort_order=_param_get(params, "sortOrder", "sort_order"),
  79. )
  80. def _process_board_kwargs(params: dict) -> dict:
  81. return dict(
  82. q=_param_get(params, "q"),
  83. apply_no=_param_get(params, "applyNo", "apply_no"),
  84. license_plate=_param_get(params, "licensePlate", "license_plate"),
  85. maintenance_type=_param_get(params, "maintenanceType", "maintenance_type"),
  86. apply_status=_param_get(params, "applyStatus", "apply_status"),
  87. apply_status_in=_param_get(params, "applyStatusIn", "apply_status_in"),
  88. apply_date_from=_param_get(params, "applyDateFrom", "apply_date_from"),
  89. apply_date_to=_param_get(params, "applyDateTo", "apply_date_to"),
  90. process_date_from=_param_get(params, "processDateFrom", "process_date_from"),
  91. process_date_to=_param_get(params, "processDateTo", "process_date_to"),
  92. start_date_from=_param_get(params, "startDateFrom", "start_date_from"),
  93. start_date_to=_param_get(params, "startDateTo", "start_date_to"),
  94. expected_pickup_date_from=_param_get(
  95. params, "expectedPickupDateFrom", "expected_pickup_date_from"
  96. ),
  97. expected_pickup_date_to=_param_get(
  98. params, "expectedPickupDateTo", "expected_pickup_date_to"
  99. ),
  100. applicant_name=_param_get(params, "applicantName", "applicant_name"),
  101. process_no=_param_get(params, "processNo", "process_no"),
  102. amount_op=_param_get(params, "amountOp", "amount_op"),
  103. amount=_param_get(params, "amount"),
  104. amount_min=_param_get(params, "amountMin", "amount_min"),
  105. amount_max=_param_get(params, "amountMax", "amount_max"),
  106. handler_name=_param_get(params, "handlerName", "handler_name"),
  107. sort_field=_param_get(params, "sortField", "sort_field"),
  108. sort_order=_param_get(params, "sortOrder", "sort_order"),
  109. )
  110. @apply_router.get("/", response_model=VehicleMaintenanceApplyListResponse)
  111. def list_applies(
  112. request: Request,
  113. q: Optional[str] = Query(default=None),
  114. maintenance_type: Optional[str] = Query(default=None, alias="maintenanceType"),
  115. status: Optional[str] = Query(default=None),
  116. apply_date_from: Optional[date] = Query(default=None, alias="applyDateFrom"),
  117. apply_date_to: Optional[date] = Query(default=None, alias="applyDateTo"),
  118. applicant_id: Optional[str] = Query(default=None, alias="applicantId"),
  119. page: int = Query(default=1, ge=1),
  120. page_size: int = Query(default=20, ge=1, le=500, alias="pageSize"),
  121. db: Session = Depends(get_db),
  122. user: DbUserContext = Depends(get_db_current_user),
  123. ) -> VehicleMaintenanceApplyListResponse:
  124. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  125. params = _merge_query(
  126. request,
  127. q=q,
  128. maintenanceType=maintenance_type,
  129. status=status,
  130. applyDateFrom=apply_date_from,
  131. applyDateTo=apply_date_to,
  132. applicantId=applicant_id,
  133. )
  134. items, total = svc.list_applies(
  135. db,
  136. tenant_no,
  137. user,
  138. **_apply_list_kwargs(params),
  139. page=page,
  140. page_size=page_size,
  141. )
  142. return VehicleMaintenanceApplyListResponse(
  143. items=items, total=total, page=page, pageSize=page_size
  144. )
  145. @apply_router.get("/reminders", response_model=list[VehicleMaintenanceApplyReminder])
  146. def list_reminders(
  147. license_plate: str = Query(..., alias="licensePlate"),
  148. exclude_apply_id: Optional[str] = Query(default=None, alias="excludeApplyId"),
  149. db: Session = Depends(get_db),
  150. user: DbUserContext = Depends(get_db_current_user),
  151. ) -> list[VehicleMaintenanceApplyReminder]:
  152. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  153. return svc.list_reminders(
  154. db,
  155. tenant_no,
  156. user,
  157. license_plate,
  158. exclude_apply_id=exclude_apply_id,
  159. )
  160. MAINT_APPLY_EXPORT_COLUMNS = [
  161. ExportColumn(title="申请单号", field="applyNo"),
  162. ExportColumn(title="车牌号", field="licensePlate"),
  163. ExportColumn(title="维保类型", field="maintenanceType"),
  164. ExportColumn(title="申请日期", field="applyDate"),
  165. ExportColumn(title="开始日期", field="startDate"),
  166. ExportColumn(title="预计取车", field="expectedPickupDate"),
  167. ExportColumn(title="申请人", field="applicantName"),
  168. ExportColumn(title="状态", field="status"),
  169. ExportColumn(title="创建时间", field="createTime"),
  170. ]
  171. class MaintenanceApplyExportBody(BaseModel):
  172. q: Optional[str] = None
  173. maintenance_type: Optional[str] = Field(default=None, alias="maintenanceType")
  174. status: Optional[str] = None
  175. apply_date_from: Optional[date] = Field(default=None, alias="applyDateFrom")
  176. apply_date_to: Optional[date] = Field(default=None, alias="applyDateTo")
  177. ids: Optional[List[str]] = None
  178. columns: Optional[List[str]] = None
  179. model_config = {"populate_by_name": True, "extra": "allow"}
  180. @apply_router.post("/export")
  181. def export_applies(
  182. body: MaintenanceApplyExportBody,
  183. db: Session = Depends(get_db),
  184. user: DbUserContext = Depends(get_db_current_user),
  185. ):
  186. """导出维保申请列表(当前筛选全量,或 ids 已选)。"""
  187. try:
  188. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  189. params = _body_to_params(body)
  190. items, _ = svc.list_applies(
  191. db,
  192. tenant_no,
  193. user,
  194. **_apply_list_kwargs(params),
  195. ids=body.ids,
  196. page=1,
  197. page_size=EXPORT_MAX_ROWS,
  198. max_page_size=EXPORT_MAX_ROWS,
  199. )
  200. rows = []
  201. for item in items:
  202. data = item.model_dump(by_alias=True)
  203. for k in ("applyDate", "startDate", "expectedPickupDate", "createTime"):
  204. if data.get(k) is not None:
  205. data[k] = str(data[k])[:19]
  206. rows.append(data)
  207. cols = filter_export_columns(MAINT_APPLY_EXPORT_COLUMNS, body.columns)
  208. date_str = datetime.now().strftime("%Y%m%d")
  209. return export_rows_to_xlsx_response(
  210. db,
  211. tenant_no=tenant_no,
  212. sheet_title="维保申请",
  213. columns=cols,
  214. rows=rows,
  215. filename_stem=f"维保申请_{date_str}",
  216. )
  217. except ExportValidationError as e:
  218. raise HTTPException(status_code=400, detail=str(e)) from e
  219. @apply_router.get("/{id}", response_model=VehicleMaintenanceApplyRead)
  220. def get_apply(
  221. id: str,
  222. db: Session = Depends(get_db),
  223. user: DbUserContext = Depends(get_db_current_user),
  224. ) -> VehicleMaintenanceApplyRead:
  225. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  226. return svc.get_apply(db, tenant_no, user, id)
  227. @apply_router.post("/", response_model=VehicleMaintenanceApplyRead, status_code=status.HTTP_201_CREATED)
  228. def create_apply(
  229. payload: VehicleMaintenanceApplyCreate,
  230. db: Session = Depends(get_db),
  231. user: DbUserContext = Depends(get_db_current_user),
  232. ) -> VehicleMaintenanceApplyRead:
  233. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  234. return svc.create_apply(db, tenant_no, user, payload)
  235. @apply_router.put("/{id}", response_model=VehicleMaintenanceApplyRead)
  236. def update_apply(
  237. id: str,
  238. payload: VehicleMaintenanceApplyUpdate,
  239. db: Session = Depends(get_db),
  240. user: DbUserContext = Depends(get_db_current_user),
  241. ) -> VehicleMaintenanceApplyRead:
  242. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  243. return svc.update_apply(db, tenant_no, user, id, payload)
  244. @apply_router.post("/{id}/cancel", response_model=VehicleMaintenanceApplyRead)
  245. def cancel_apply(
  246. id: str,
  247. body: Optional[CancelReasonBody] = Body(default=None),
  248. db: Session = Depends(get_db),
  249. user: DbUserContext = Depends(get_db_current_user),
  250. ) -> VehicleMaintenanceApplyRead:
  251. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  252. return svc.cancel_apply(
  253. db, tenant_no, user, id, cancel_reason=body.cancel_reason if body else None
  254. )
  255. @apply_router.delete("/{ids}", status_code=status.HTTP_204_NO_CONTENT)
  256. def delete_applies(
  257. ids: str,
  258. db: Session = Depends(get_db),
  259. user: DbUserContext = Depends(get_db_current_user),
  260. ) -> None:
  261. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  262. apply_ids = [item for item in ids.split(",") if item]
  263. svc.delete_applies(db, tenant_no, user, apply_ids)
  264. @process_router.get("/process-board", response_model=VehicleMaintenanceProcessBoardListResponse)
  265. def list_process_board(
  266. request: Request,
  267. q: Optional[str] = Query(default=None),
  268. maintenance_type: Optional[str] = Query(default=None, alias="maintenanceType"),
  269. apply_status: Optional[str] = Query(default=None, alias="applyStatus"),
  270. apply_date_from: Optional[date] = Query(default=None, alias="applyDateFrom"),
  271. apply_date_to: Optional[date] = Query(default=None, alias="applyDateTo"),
  272. process_date_from: Optional[date] = Query(default=None, alias="processDateFrom"),
  273. process_date_to: Optional[date] = Query(default=None, alias="processDateTo"),
  274. page: int = Query(default=1, ge=1),
  275. page_size: int = Query(default=20, ge=1, le=500, alias="pageSize"),
  276. db: Session = Depends(get_db),
  277. user: DbUserContext = Depends(get_db_current_user),
  278. ) -> VehicleMaintenanceProcessBoardListResponse:
  279. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  280. params = _merge_query(
  281. request,
  282. q=q,
  283. maintenanceType=maintenance_type,
  284. applyStatus=apply_status,
  285. applyDateFrom=apply_date_from,
  286. applyDateTo=apply_date_to,
  287. processDateFrom=process_date_from,
  288. processDateTo=process_date_to,
  289. )
  290. items, total = svc.list_process_board(
  291. db,
  292. tenant_no,
  293. user,
  294. **_process_board_kwargs(params),
  295. page=page,
  296. page_size=page_size,
  297. )
  298. return VehicleMaintenanceProcessBoardListResponse(
  299. items=items, total=total, page=page, pageSize=page_size
  300. )
  301. MAINT_PROCESS_EXPORT_COLUMNS = [
  302. ExportColumn(title="申请单号", field="applyNo"),
  303. ExportColumn(title="申请日期", field="applyDate"),
  304. ExportColumn(title="申请人", field="applicantName"),
  305. ExportColumn(title="申请状态", field="applyStatus"),
  306. ExportColumn(title="车牌号", field="licensePlate"),
  307. ExportColumn(title="维保类型", field="maintenanceType"),
  308. ExportColumn(title="处理单号", field="processNo"),
  309. ExportColumn(title="处理日期", field="processDate"),
  310. ExportColumn(title="处理人", field="handlerName"),
  311. ExportColumn(title="金额(元)", field="amount"),
  312. ]
  313. class MaintProcessExportBody(BaseModel):
  314. q: Optional[str] = None
  315. maintenance_type: Optional[str] = Field(default=None, alias="maintenanceType")
  316. apply_status: Optional[str] = Field(default=None, alias="applyStatus")
  317. apply_date_from: Optional[date] = Field(default=None, alias="applyDateFrom")
  318. apply_date_to: Optional[date] = Field(default=None, alias="applyDateTo")
  319. process_date_from: Optional[date] = Field(default=None, alias="processDateFrom")
  320. process_date_to: Optional[date] = Field(default=None, alias="processDateTo")
  321. ids: Optional[List[str]] = None
  322. columns: Optional[List[str]] = None
  323. model_config = {"populate_by_name": True, "extra": "allow"}
  324. @process_router.post("/process-board/export")
  325. def export_process_board(
  326. body: MaintProcessExportBody,
  327. db: Session = Depends(get_db),
  328. user: DbUserContext = Depends(get_db_current_user),
  329. ):
  330. try:
  331. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  332. params = _body_to_params(body)
  333. items, _ = svc.list_process_board(
  334. db,
  335. tenant_no,
  336. user,
  337. **_process_board_kwargs(params),
  338. ids=body.ids,
  339. page=1,
  340. page_size=EXPORT_MAX_ROWS,
  341. max_page_size=EXPORT_MAX_ROWS,
  342. )
  343. rows = []
  344. for item in items:
  345. data = item.model_dump(by_alias=True)
  346. for k, v in list(data.items()):
  347. if hasattr(v, 'isoformat'):
  348. data[k] = str(v)[:19]
  349. rows.append(data)
  350. cols = filter_export_columns(MAINT_PROCESS_EXPORT_COLUMNS, body.columns)
  351. date_str = datetime.now().strftime("%Y%m%d")
  352. return export_rows_to_xlsx_response(
  353. db, tenant_no=tenant_no, sheet_title="维保过程", columns=cols, rows=rows,
  354. filename_stem=f"维保过程_{date_str}",
  355. )
  356. except ExportValidationError as e:
  357. raise HTTPException(status_code=400, detail=str(e)) from e
  358. @process_router.get("/{id}", response_model=VehicleMaintenanceProcessRead)
  359. def get_process(
  360. id: str,
  361. db: Session = Depends(get_db),
  362. user: DbUserContext = Depends(get_db_current_user),
  363. ) -> VehicleMaintenanceProcessRead:
  364. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  365. return svc.get_process(db, tenant_no, user, id)
  366. @process_router.post("/", response_model=VehicleMaintenanceProcessRead, status_code=status.HTTP_201_CREATED)
  367. def create_process(
  368. payload: VehicleMaintenanceProcessCreate,
  369. db: Session = Depends(get_db),
  370. user: DbUserContext = Depends(get_db_current_user),
  371. ) -> VehicleMaintenanceProcessRead:
  372. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  373. return svc.create_process(db, tenant_no, user, payload)
  374. @process_router.put("/{id}", response_model=VehicleMaintenanceProcessRead)
  375. def update_process(
  376. id: str,
  377. payload: VehicleMaintenanceProcessUpdate,
  378. db: Session = Depends(get_db),
  379. user: DbUserContext = Depends(get_db_current_user),
  380. ) -> VehicleMaintenanceProcessRead:
  381. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  382. return svc.update_process(db, tenant_no, user, id, payload)
  383. @process_router.delete("/{ids}", status_code=status.HTTP_204_NO_CONTENT)
  384. def delete_processes(
  385. ids: str,
  386. db: Session = Depends(get_db),
  387. user: DbUserContext = Depends(get_db_current_user),
  388. ) -> None:
  389. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  390. process_ids = [item for item in ids.split(",") if item]
  391. svc.delete_processes(db, tenant_no, user, process_ids)