pl_cert_required_service.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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. subject_label: str = "",
  130. ) -> None:
  131. ptype = (personnel_type or "").strip()
  132. if not ptype:
  133. raise HTTPException(status_code=400, detail="人员类型不能为空")
  134. required = get_required_personnel_cert_names(ptype)
  135. errors = collect_required_cert_errors(cert_rows, required)
  136. if errors:
  137. prefix = f"人员{subject_label}" if subject_label else "人员"
  138. raise HTTPException(
  139. status_code=400,
  140. detail=f"必备证书未齐全:{prefix}{';'.join(errors)}",
  141. )
  142. def validate_self_vehicle_required_certs(
  143. cert_rows: Sequence[CertRow | dict],
  144. *,
  145. trailer_no: str | None = None,
  146. ) -> None:
  147. errors: list[str] = []
  148. errors.extend(
  149. collect_required_cert_errors(
  150. cert_rows, SELF_HEAD_REQUIRED, part_label="主车"
  151. )
  152. )
  153. if (trailer_no or "").strip():
  154. errors.extend(
  155. collect_required_cert_errors(
  156. cert_rows, SELF_TRAILER_REQUIRED, part_label="挂车"
  157. )
  158. )
  159. if errors:
  160. raise HTTPException(
  161. status_code=400,
  162. detail=f"必备证书未齐全:{';'.join(errors)}",
  163. )
  164. def validate_carrier_vehicle_required_certs(
  165. cert_rows: Sequence[CertRow | dict],
  166. *,
  167. trailer_no: str | None = None,
  168. ) -> None:
  169. errors: list[str] = []
  170. errors.extend(
  171. collect_required_cert_errors(
  172. cert_rows, CARRIER_HEAD_REQUIRED, part_label="主车"
  173. )
  174. )
  175. if (trailer_no or "").strip():
  176. errors.extend(
  177. collect_required_cert_errors(
  178. cert_rows, CARRIER_TRAILER_REQUIRED, part_label="挂车"
  179. )
  180. )
  181. if errors:
  182. raise HTTPException(
  183. status_code=400,
  184. detail=f"必备证书未齐全:{';'.join(errors)}",
  185. )
  186. def validate_vehicle_personnel_affiliation(
  187. db: Session,
  188. tenant_no: str,
  189. vehicle_type: str | None,
  190. personnel_rows: Sequence[VehiclePersonnelRow],
  191. ) -> None:
  192. vtype = (vehicle_type or "").strip()
  193. if vtype not in (VEHICLE_TYPE_SELF, VEHICLE_TYPE_CARRIER):
  194. return
  195. label = "山峡" if vtype == VEHICLE_TYPE_SELF else "非山峡"
  196. for row in personnel_rows:
  197. personnel_id = (row.personnel_id or "").strip()
  198. if not personnel_id:
  199. continue
  200. personnel = (
  201. db.query(PlPersonnel)
  202. .filter(
  203. PlPersonnel.id == personnel_id,
  204. PlPersonnel.tenant_no == tenant_no,
  205. PlPersonnel.del_flag == 0,
  206. )
  207. .first()
  208. )
  209. if not personnel:
  210. continue
  211. affiliation = (personnel.personnel_affiliation or VEHICLE_TYPE_SELF).strip()
  212. if affiliation != vtype:
  213. name = (row.personnel_name or personnel.personnel_name or personnel_id).strip()
  214. raise HTTPException(
  215. status_code=400,
  216. detail=f"关联人员「{name}」归属与车辆类型不匹配,{label}车辆仅可关联{label}人员",
  217. )
  218. def validate_vehicle_personnel_cert_links(
  219. db: Session,
  220. tenant_no: str,
  221. personnel_rows: Sequence[VehiclePersonnelRow],
  222. ) -> None:
  223. for row in personnel_rows:
  224. personnel_id = (row.personnel_id or "").strip()
  225. if not personnel_id:
  226. continue
  227. personnel = (
  228. db.query(PlPersonnel)
  229. .filter(
  230. PlPersonnel.id == personnel_id,
  231. PlPersonnel.tenant_no == tenant_no,
  232. PlPersonnel.del_flag == 0,
  233. )
  234. .first()
  235. )
  236. if not personnel:
  237. continue
  238. validate_personnel_type_role_compat(personnel.personnel_type, row.role)
  239. cert_rows = list_owner_certs(db, tenant_no, "personnel", personnel_id)
  240. role = (row.role or "").strip()
  241. required = (
  242. PERSONNEL_DRIVER_REQUIRED
  243. if role == "driver"
  244. else PERSONNEL_ESCORT_REQUIRED
  245. )
  246. errors = collect_required_cert_errors(cert_rows, required)
  247. if errors:
  248. name = (row.personnel_name or personnel.personnel_name or personnel_id).strip()
  249. raise HTTPException(
  250. status_code=400,
  251. detail=(
  252. f"关联人员「{name}」必备证书未齐全:{';'.join(errors)}"
  253. ),
  254. )
  255. def validate_vehicle_save_certs(
  256. db: Session,
  257. tenant_no: str,
  258. *,
  259. vehicle_type: str | None,
  260. trailer_no: str | None,
  261. cert_rows: Sequence[CertRow | dict],
  262. personnel_rows: Sequence[VehiclePersonnelRow] | None = None,
  263. ) -> None:
  264. vtype = (vehicle_type or "").strip()
  265. if vtype == VEHICLE_TYPE_SELF:
  266. validate_self_vehicle_required_certs(cert_rows, trailer_no=trailer_no)
  267. elif vtype == VEHICLE_TYPE_CARRIER:
  268. validate_carrier_vehicle_required_certs(cert_rows, trailer_no=trailer_no)
  269. if vtype in (VEHICLE_TYPE_SELF, VEHICLE_TYPE_CARRIER) and personnel_rows:
  270. validate_vehicle_personnel_affiliation(
  271. db, tenant_no, vtype, personnel_rows
  272. )
  273. validate_vehicle_personnel_cert_links(db, tenant_no, personnel_rows)