pl_cert_required_service.py 10 KB

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