| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- """运输计划 - 必备证书校验"""
- from __future__ import annotations
- from typing import Iterable, Optional, Sequence
- from fastapi import HTTPException
- from sqlalchemy.orm import Session
- from app.models.pl_models import PlPersonnel
- from app.schemas.pl.base_data import CertRow, VehiclePersonnelRow
- from app.services.pl_cert_service import list_owner_certs
- VEHICLE_TYPE_SELF = "self"
- VEHICLE_TYPE_CARRIER = "carrier"
- SELF_HEAD_REQUIRED = ("行驶证", "营运证", "保险单")
- SELF_TRAILER_REQUIRED = ("行驶证", "营运证", "保险单", "罐检报告")
- CARRIER_HEAD_REQUIRED = ("行驶证", "营运证")
- CARRIER_TRAILER_REQUIRED = ("行驶证", "营运证")
- PERSONNEL_DRIVER_REQUIRED = ("身份证", "驾驶证", "驾驶员从业资格证")
- PERSONNEL_ESCORT_REQUIRED = ("身份证", "押运员从业资格证")
- PERSONNEL_BOTH_REQUIRED = (
- "身份证",
- "驾驶证",
- "驾驶员从业资格证",
- "押运员从业资格证",
- )
- VEHICLE_PART_HEAD = "head"
- VEHICLE_PART_TRAILER = "trailer"
- def match_required_cert_name(actual_name: str | None, required_name: str) -> bool:
- name = (actual_name or "").strip()
- if not name:
- return False
- if required_name == "押运员从业资格证":
- return name == "押运员从业资格证" or name == "押运员证"
- return name == required_name
- def _cert_row_complete(cert: CertRow | dict, required_name: str) -> bool:
- if isinstance(cert, dict):
- cert_name = cert.get("cert_name") or cert.get("certName")
- expire_date = cert.get("expire_date") or cert.get("expireDate")
- attachment_urls = cert.get("attachment_urls") or cert.get("attachmentUrls")
- else:
- cert_name = cert.cert_name
- expire_date = cert.expire_date
- attachment_urls = cert.attachment_urls
- if not match_required_cert_name(cert_name, required_name):
- return False
- if not expire_date:
- return False
- if not (attachment_urls or "").strip():
- return False
- return True
- def _find_cert_for_required(
- cert_rows: Sequence[CertRow | dict],
- required_name: str,
- *,
- vehicle_part: str | None = None,
- ) -> CertRow | dict | None:
- for cert in cert_rows:
- if isinstance(cert, dict):
- name = cert.get("cert_name") or cert.get("certName")
- part = cert.get("vehicle_part") or cert.get("vehiclePart")
- else:
- name = cert.cert_name
- part = cert.vehicle_part
- if vehicle_part is not None and (part or VEHICLE_PART_HEAD) != vehicle_part:
- continue
- if match_required_cert_name(name, required_name):
- return cert
- return None
- def _missing_field_labels(cert: CertRow | dict | None, required_name: str) -> list[str]:
- if cert is None:
- return ["有效期", "附件"]
- missing: list[str] = []
- if isinstance(cert, dict):
- expire_date = cert.get("expire_date") or cert.get("expireDate")
- attachment_urls = cert.get("attachment_urls") or cert.get("attachmentUrls")
- cert_name = cert.get("cert_name") or cert.get("certName")
- else:
- expire_date = cert.expire_date
- attachment_urls = cert.attachment_urls
- cert_name = cert.cert_name
- if not match_required_cert_name(cert_name, required_name):
- return ["有效期", "附件"]
- if not expire_date:
- missing.append("有效期")
- if not (attachment_urls or "").strip():
- missing.append("附件")
- return missing
- def collect_required_cert_errors(
- cert_rows: Sequence[CertRow | dict],
- required_names: Iterable[str],
- *,
- part_label: str = "",
- ) -> list[str]:
- errors: list[str] = []
- prefix = f"{part_label}" if part_label else ""
- vehicle_part = None
- if part_label in ("主车", "head"):
- vehicle_part = VEHICLE_PART_HEAD
- elif part_label in ("挂车", "trailer"):
- vehicle_part = VEHICLE_PART_TRAILER
- for required_name in required_names:
- cert = _find_cert_for_required(
- cert_rows, required_name, vehicle_part=vehicle_part
- )
- missing = _missing_field_labels(cert, required_name)
- if missing:
- label = f"{prefix}「{required_name}」" if prefix else f"「{required_name}」"
- errors.append(f"{label}缺少:{'、'.join(missing)}")
- return errors
- def get_required_personnel_cert_names(personnel_type: str | None) -> tuple[str, ...]:
- ptype = (personnel_type or "").strip()
- if ptype == "escort":
- return PERSONNEL_ESCORT_REQUIRED
- if ptype == "driver":
- return PERSONNEL_DRIVER_REQUIRED
- if ptype == "both":
- return PERSONNEL_BOTH_REQUIRED
- return PERSONNEL_BOTH_REQUIRED
- def validate_personnel_type_role_compat(personnel_type: str | None, role: str | None) -> None:
- ptype = (personnel_type or "").strip()
- r = (role or "").strip()
- if not ptype or not r:
- return
- if ptype == "driver" and r == "escort":
- raise HTTPException(status_code=400, detail="驾驶员类型人员不可关联为押运员")
- if ptype == "escort" and r == "driver":
- raise HTTPException(status_code=400, detail="押运员类型人员不可关联为驾驶员")
- def validate_personnel_required_certs(
- cert_rows: Sequence[CertRow | dict],
- personnel_type: str | None,
- *,
- personnel_affiliation: str | None = None,
- subject_label: str = "",
- ) -> None:
- affiliation = (personnel_affiliation or VEHICLE_TYPE_SELF).strip()
- if affiliation == VEHICLE_TYPE_CARRIER:
- return
- ptype = (personnel_type or "").strip()
- if not ptype:
- raise HTTPException(status_code=400, detail="人员类型不能为空")
- required = get_required_personnel_cert_names(ptype)
- errors = collect_required_cert_errors(cert_rows, required)
- if errors:
- prefix = f"人员{subject_label}" if subject_label else "人员"
- raise HTTPException(
- status_code=400,
- detail=f"必备证书未齐全:{prefix}{';'.join(errors)}",
- )
- def validate_self_vehicle_required_certs(
- cert_rows: Sequence[CertRow | dict],
- *,
- trailer_no: str | None = None,
- ) -> None:
- errors: list[str] = []
- errors.extend(
- collect_required_cert_errors(
- cert_rows, SELF_HEAD_REQUIRED, part_label="主车"
- )
- )
- if (trailer_no or "").strip():
- errors.extend(
- collect_required_cert_errors(
- cert_rows, SELF_TRAILER_REQUIRED, part_label="挂车"
- )
- )
- if errors:
- raise HTTPException(
- status_code=400,
- detail=f"必备证书未齐全:{';'.join(errors)}",
- )
- def validate_carrier_vehicle_required_certs(
- cert_rows: Sequence[CertRow | dict],
- *,
- trailer_no: str | None = None,
- ) -> None:
- errors: list[str] = []
- errors.extend(
- collect_required_cert_errors(
- cert_rows, CARRIER_HEAD_REQUIRED, part_label="主车"
- )
- )
- if (trailer_no or "").strip():
- errors.extend(
- collect_required_cert_errors(
- cert_rows, CARRIER_TRAILER_REQUIRED, part_label="挂车"
- )
- )
- if errors:
- raise HTTPException(
- status_code=400,
- detail=f"必备证书未齐全:{';'.join(errors)}",
- )
- def validate_vehicle_personnel_affiliation(
- db: Session,
- tenant_no: str,
- vehicle_type: str | None,
- personnel_rows: Sequence[VehiclePersonnelRow],
- ) -> None:
- vtype = (vehicle_type or "").strip()
- if vtype not in (VEHICLE_TYPE_SELF, VEHICLE_TYPE_CARRIER):
- return
- label = "山峡" if vtype == VEHICLE_TYPE_SELF else "非山峡"
- for row in personnel_rows:
- personnel_id = (row.personnel_id or "").strip()
- if not personnel_id:
- continue
- personnel = (
- db.query(PlPersonnel)
- .filter(
- PlPersonnel.id == personnel_id,
- PlPersonnel.tenant_no == tenant_no,
- PlPersonnel.del_flag == 0,
- )
- .first()
- )
- if not personnel:
- continue
- affiliation = (personnel.personnel_affiliation or VEHICLE_TYPE_SELF).strip()
- if affiliation != vtype:
- name = (row.personnel_name or personnel.personnel_name or personnel_id).strip()
- raise HTTPException(
- status_code=400,
- detail=f"关联人员「{name}」归属与车辆类型不匹配,{label}车辆仅可关联{label}人员",
- )
- def validate_vehicle_personnel_cert_links(
- db: Session,
- tenant_no: str,
- personnel_rows: Sequence[VehiclePersonnelRow],
- ) -> None:
- for row in personnel_rows:
- personnel_id = (row.personnel_id or "").strip()
- if not personnel_id:
- continue
- personnel = (
- db.query(PlPersonnel)
- .filter(
- PlPersonnel.id == personnel_id,
- PlPersonnel.tenant_no == tenant_no,
- PlPersonnel.del_flag == 0,
- )
- .first()
- )
- if not personnel:
- continue
- validate_personnel_type_role_compat(personnel.personnel_type, row.role)
- cert_rows = list_owner_certs(db, tenant_no, "personnel", personnel_id)
- role = (row.role or "").strip()
- required = (
- PERSONNEL_DRIVER_REQUIRED
- if role == "driver"
- else PERSONNEL_ESCORT_REQUIRED
- )
- errors = collect_required_cert_errors(cert_rows, required)
- if errors:
- name = (row.personnel_name or personnel.personnel_name or personnel_id).strip()
- raise HTTPException(
- status_code=400,
- detail=(
- f"关联人员「{name}」必备证书未齐全:{';'.join(errors)}"
- ),
- )
- def validate_vehicle_save_certs(
- db: Session,
- tenant_no: str,
- *,
- vehicle_type: str | None,
- trailer_no: str | None,
- cert_rows: Sequence[CertRow | dict],
- personnel_rows: Sequence[VehiclePersonnelRow] | None = None,
- ) -> None:
- vtype = (vehicle_type or "").strip()
- if vtype == VEHICLE_TYPE_SELF:
- validate_self_vehicle_required_certs(cert_rows, trailer_no=trailer_no)
- if vtype in (VEHICLE_TYPE_SELF, VEHICLE_TYPE_CARRIER) and personnel_rows:
- validate_vehicle_personnel_affiliation(
- db, tenant_no, vtype, personnel_rows
- )
- if vtype == VEHICLE_TYPE_SELF:
- validate_vehicle_personnel_cert_links(db, tenant_no, personnel_rows)
|