pl_cert_required_service.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. """运输计划 - 必备证书校验"""
  2. from __future__ import annotations
  3. from typing import Iterable, Optional, Sequence
  4. from fastapi import HTTPException
  5. from sqlalchemy.orm import Session
  6. from app.models.pl_models import PlPersonnel
  7. from app.schemas.pl.base_data import CertRow, VehiclePersonnelRow
  8. from app.services.pl_cert_service import list_owner_certs
  9. VEHICLE_TYPE_SELF = "self"
  10. VEHICLE_TYPE_CARRIER = "carrier"
  11. SELF_HEAD_REQUIRED = ("行驶证", "营运证", "保险单")
  12. SELF_TRAILER_REQUIRED = ("行驶证", "营运证", "保险单", "罐检报告")
  13. CARRIER_HEAD_REQUIRED = ("行驶证", "营运证")
  14. CARRIER_TRAILER_REQUIRED = ("行驶证", "营运证")
  15. PERSONNEL_DRIVER_REQUIRED = ("身份证", "驾驶证", "驾驶员从业资格证")
  16. PERSONNEL_ESCORT_REQUIRED = ("身份证", "押运员从业资格证")
  17. PERSONNEL_BOTH_REQUIRED = (
  18. "身份证",
  19. "驾驶证",
  20. "驾驶员从业资格证",
  21. "押运员从业资格证",
  22. )
  23. VEHICLE_PART_HEAD = "head"
  24. VEHICLE_PART_TRAILER = "trailer"
  25. def match_required_cert_name(actual_name: str | None, required_name: str) -> bool:
  26. name = (actual_name or "").strip()
  27. if not name:
  28. return False
  29. if required_name == "押运员从业资格证":
  30. return name == "押运员从业资格证" or name == "押运员证"
  31. return name == required_name
  32. def _cert_row_complete(cert: CertRow | dict, required_name: str) -> bool:
  33. if isinstance(cert, dict):
  34. cert_name = cert.get("cert_name") or cert.get("certName")
  35. expire_date = cert.get("expire_date") or cert.get("expireDate")
  36. attachment_urls = cert.get("attachment_urls") or cert.get("attachmentUrls")
  37. else:
  38. cert_name = cert.cert_name
  39. expire_date = cert.expire_date
  40. attachment_urls = cert.attachment_urls
  41. if not match_required_cert_name(cert_name, required_name):
  42. return False
  43. if not expire_date:
  44. return False
  45. if not (attachment_urls or "").strip():
  46. return False
  47. return True
  48. def _find_cert_for_required(
  49. cert_rows: Sequence[CertRow | dict],
  50. required_name: str,
  51. *,
  52. vehicle_part: str | None = None,
  53. ) -> CertRow | dict | None:
  54. for cert in cert_rows:
  55. if isinstance(cert, dict):
  56. name = cert.get("cert_name") or cert.get("certName")
  57. part = cert.get("vehicle_part") or cert.get("vehiclePart")
  58. else:
  59. name = cert.cert_name
  60. part = cert.vehicle_part
  61. if vehicle_part is not None and (part or VEHICLE_PART_HEAD) != vehicle_part:
  62. continue
  63. if match_required_cert_name(name, required_name):
  64. return cert
  65. return None
  66. def _missing_field_labels(cert: CertRow | dict | None, required_name: str) -> list[str]:
  67. if cert is None:
  68. return ["有效期", "附件"]
  69. missing: list[str] = []
  70. if isinstance(cert, dict):
  71. expire_date = cert.get("expire_date") or cert.get("expireDate")
  72. attachment_urls = cert.get("attachment_urls") or cert.get("attachmentUrls")
  73. cert_name = cert.get("cert_name") or cert.get("certName")
  74. else:
  75. expire_date = cert.expire_date
  76. attachment_urls = cert.attachment_urls
  77. cert_name = cert.cert_name
  78. if not match_required_cert_name(cert_name, required_name):
  79. return ["有效期", "附件"]
  80. if not expire_date:
  81. missing.append("有效期")
  82. if not (attachment_urls or "").strip():
  83. missing.append("附件")
  84. return missing
  85. def collect_required_cert_errors(
  86. cert_rows: Sequence[CertRow | dict],
  87. required_names: Iterable[str],
  88. *,
  89. part_label: str = "",
  90. ) -> list[str]:
  91. errors: list[str] = []
  92. prefix = f"{part_label}" if part_label else ""
  93. vehicle_part = None
  94. if part_label in ("主车", "head"):
  95. vehicle_part = VEHICLE_PART_HEAD
  96. elif part_label in ("挂车", "trailer"):
  97. vehicle_part = VEHICLE_PART_TRAILER
  98. for required_name in required_names:
  99. cert = _find_cert_for_required(
  100. cert_rows, required_name, vehicle_part=vehicle_part
  101. )
  102. missing = _missing_field_labels(cert, required_name)
  103. if missing:
  104. label = f"{prefix}「{required_name}」" if prefix else f"「{required_name}」"
  105. errors.append(f"{label}缺少:{'、'.join(missing)}")
  106. return errors
  107. def get_required_personnel_cert_names(personnel_type: str | None) -> tuple[str, ...]:
  108. ptype = (personnel_type or "").strip()
  109. if ptype == "escort":
  110. return PERSONNEL_ESCORT_REQUIRED
  111. if ptype == "driver":
  112. return PERSONNEL_DRIVER_REQUIRED
  113. if ptype == "both":
  114. return PERSONNEL_BOTH_REQUIRED
  115. return PERSONNEL_BOTH_REQUIRED
  116. def validate_personnel_type_role_compat(personnel_type: str | None, role: str | None) -> None:
  117. ptype = (personnel_type or "").strip()
  118. r = (role or "").strip()
  119. if not ptype or not r:
  120. return
  121. if ptype == "driver" and r == "escort":
  122. raise HTTPException(status_code=400, detail="驾驶员类型人员不可关联为押运员")
  123. if ptype == "escort" and r == "driver":
  124. raise HTTPException(status_code=400, detail="押运员类型人员不可关联为驾驶员")
  125. def validate_personnel_required_certs(
  126. cert_rows: Sequence[CertRow | dict],
  127. personnel_type: str | None,
  128. *,
  129. personnel_affiliation: str | None = None,
  130. subject_label: str = "",
  131. ) -> None:
  132. affiliation = (personnel_affiliation or VEHICLE_TYPE_SELF).strip()
  133. if affiliation == VEHICLE_TYPE_CARRIER:
  134. return
  135. ptype = (personnel_type or "").strip()
  136. if not ptype:
  137. raise HTTPException(status_code=400, detail="人员类型不能为空")
  138. required = get_required_personnel_cert_names(ptype)
  139. errors = collect_required_cert_errors(cert_rows, required)
  140. if errors:
  141. prefix = f"人员{subject_label}" if subject_label else "人员"
  142. raise HTTPException(
  143. status_code=400,
  144. detail=f"必备证书未齐全:{prefix}{';'.join(errors)}",
  145. )
  146. def validate_self_vehicle_required_certs(
  147. cert_rows: Sequence[CertRow | dict],
  148. *,
  149. trailer_no: str | None = None,
  150. ) -> None:
  151. errors: list[str] = []
  152. errors.extend(
  153. collect_required_cert_errors(
  154. cert_rows, SELF_HEAD_REQUIRED, part_label="主车"
  155. )
  156. )
  157. if (trailer_no or "").strip():
  158. errors.extend(
  159. collect_required_cert_errors(
  160. cert_rows, SELF_TRAILER_REQUIRED, part_label="挂车"
  161. )
  162. )
  163. if errors:
  164. raise HTTPException(
  165. status_code=400,
  166. detail=f"必备证书未齐全:{';'.join(errors)}",
  167. )
  168. def validate_carrier_vehicle_required_certs(
  169. cert_rows: Sequence[CertRow | dict],
  170. *,
  171. trailer_no: str | None = None,
  172. ) -> None:
  173. errors: list[str] = []
  174. errors.extend(
  175. collect_required_cert_errors(
  176. cert_rows, CARRIER_HEAD_REQUIRED, part_label="主车"
  177. )
  178. )
  179. if (trailer_no or "").strip():
  180. errors.extend(
  181. collect_required_cert_errors(
  182. cert_rows, CARRIER_TRAILER_REQUIRED, part_label="挂车"
  183. )
  184. )
  185. if errors:
  186. raise HTTPException(
  187. status_code=400,
  188. detail=f"必备证书未齐全:{';'.join(errors)}",
  189. )
  190. def validate_vehicle_personnel_affiliation(
  191. db: Session,
  192. tenant_no: str,
  193. vehicle_type: str | None,
  194. personnel_rows: Sequence[VehiclePersonnelRow],
  195. ) -> None:
  196. vtype = (vehicle_type or "").strip()
  197. if vtype not in (VEHICLE_TYPE_SELF, VEHICLE_TYPE_CARRIER):
  198. return
  199. label = "山峡" if vtype == VEHICLE_TYPE_SELF else "非山峡"
  200. for row in personnel_rows:
  201. personnel_id = (row.personnel_id or "").strip()
  202. if not personnel_id:
  203. continue
  204. personnel = (
  205. db.query(PlPersonnel)
  206. .filter(
  207. PlPersonnel.id == personnel_id,
  208. PlPersonnel.tenant_no == tenant_no,
  209. PlPersonnel.del_flag == 0,
  210. )
  211. .first()
  212. )
  213. if not personnel:
  214. continue
  215. affiliation = (personnel.personnel_affiliation or VEHICLE_TYPE_SELF).strip()
  216. if affiliation != vtype:
  217. name = (row.personnel_name or personnel.personnel_name or personnel_id).strip()
  218. raise HTTPException(
  219. status_code=400,
  220. detail=f"关联人员「{name}」归属与车辆类型不匹配,{label}车辆仅可关联{label}人员",
  221. )
  222. def validate_vehicle_personnel_cert_links(
  223. db: Session,
  224. tenant_no: str,
  225. personnel_rows: Sequence[VehiclePersonnelRow],
  226. ) -> None:
  227. for row in personnel_rows:
  228. personnel_id = (row.personnel_id or "").strip()
  229. if not personnel_id:
  230. continue
  231. personnel = (
  232. db.query(PlPersonnel)
  233. .filter(
  234. PlPersonnel.id == personnel_id,
  235. PlPersonnel.tenant_no == tenant_no,
  236. PlPersonnel.del_flag == 0,
  237. )
  238. .first()
  239. )
  240. if not personnel:
  241. continue
  242. validate_personnel_type_role_compat(personnel.personnel_type, row.role)
  243. cert_rows = list_owner_certs(db, tenant_no, "personnel", personnel_id)
  244. role = (row.role or "").strip()
  245. required = (
  246. PERSONNEL_DRIVER_REQUIRED
  247. if role == "driver"
  248. else PERSONNEL_ESCORT_REQUIRED
  249. )
  250. errors = collect_required_cert_errors(cert_rows, required)
  251. if errors:
  252. name = (row.personnel_name or personnel.personnel_name or personnel_id).strip()
  253. raise HTTPException(
  254. status_code=400,
  255. detail=(
  256. f"关联人员「{name}」必备证书未齐全:{';'.join(errors)}"
  257. ),
  258. )
  259. def validate_vehicle_save_certs(
  260. db: Session,
  261. tenant_no: str,
  262. *,
  263. vehicle_type: str | None,
  264. trailer_no: str | None,
  265. cert_rows: Sequence[CertRow | dict],
  266. personnel_rows: Sequence[VehiclePersonnelRow] | None = None,
  267. ) -> None:
  268. vtype = (vehicle_type or "").strip()
  269. if vtype == VEHICLE_TYPE_SELF:
  270. validate_self_vehicle_required_certs(cert_rows, trailer_no=trailer_no)
  271. if vtype in (VEHICLE_TYPE_SELF, VEHICLE_TYPE_CARRIER) and personnel_rows:
  272. validate_vehicle_personnel_affiliation(
  273. db, tenant_no, vtype, personnel_rows
  274. )
  275. if vtype == VEHICLE_TYPE_SELF:
  276. validate_vehicle_personnel_cert_links(db, tenant_no, personnel_rows)