|
|
@@ -0,0 +1,317 @@
|
|
|
+"""运输计划 - 三方粘贴识别批量提交"""
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+from typing import Optional
|
|
|
+
|
|
|
+from fastapi import HTTPException
|
|
|
+from sqlalchemy.orm import Session
|
|
|
+
|
|
|
+from app.core.utils import generate_uuid
|
|
|
+from app.models.pl_models import PlCarrier, PlPersonnel, PlVehicle
|
|
|
+from app.schemas.pl.base_data import VehiclePersonnelRow
|
|
|
+from app.schemas.pl.paste_recognize import (
|
|
|
+ ConflictAction,
|
|
|
+ PasteCommitBody,
|
|
|
+ PasteCommitResult,
|
|
|
+ PastePersonnelIn,
|
|
|
+ PasteVehicleIn,
|
|
|
+)
|
|
|
+from app.services.pl_personnel_service import save_vehicle_personnel
|
|
|
+
|
|
|
+
|
|
|
+def _conflict_map(conflicts: list[ConflictAction]) -> dict[str, ConflictAction]:
|
|
|
+ result: dict[str, ConflictAction] = {}
|
|
|
+ for item in conflicts:
|
|
|
+ key = (item.key or "").strip()
|
|
|
+ if not key:
|
|
|
+ continue
|
|
|
+ if key in result:
|
|
|
+ raise HTTPException(status_code=400, detail=f"冲突项「{key}」重复")
|
|
|
+ result[key] = item
|
|
|
+ return result
|
|
|
+
|
|
|
+
|
|
|
+def _find_personnel_by_id_card(
|
|
|
+ db: Session, tenant_no: str, id_card_no: Optional[str]
|
|
|
+) -> Optional[PlPersonnel]:
|
|
|
+ card = (id_card_no or "").strip().upper()
|
|
|
+ if not card:
|
|
|
+ return None
|
|
|
+ return (
|
|
|
+ db.query(PlPersonnel)
|
|
|
+ .filter(
|
|
|
+ PlPersonnel.tenant_no == tenant_no,
|
|
|
+ PlPersonnel.id_card_no == card,
|
|
|
+ PlPersonnel.del_flag == 0,
|
|
|
+ )
|
|
|
+ .first()
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _find_vehicle_by_plate(
|
|
|
+ db: Session, tenant_no: str, license_plate: str
|
|
|
+) -> Optional[PlVehicle]:
|
|
|
+ plate = (license_plate or "").strip()
|
|
|
+ if not plate:
|
|
|
+ return None
|
|
|
+ return (
|
|
|
+ db.query(PlVehicle)
|
|
|
+ .filter(
|
|
|
+ PlVehicle.tenant_no == tenant_no,
|
|
|
+ PlVehicle.license_plate == plate,
|
|
|
+ PlVehicle.del_flag == 0,
|
|
|
+ )
|
|
|
+ .first()
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _get_personnel(
|
|
|
+ db: Session, tenant_no: str, personnel_id: str
|
|
|
+) -> PlPersonnel:
|
|
|
+ row = (
|
|
|
+ db.query(PlPersonnel)
|
|
|
+ .filter(
|
|
|
+ PlPersonnel.id == personnel_id,
|
|
|
+ PlPersonnel.tenant_no == tenant_no,
|
|
|
+ PlPersonnel.del_flag == 0,
|
|
|
+ )
|
|
|
+ .first()
|
|
|
+ )
|
|
|
+ if not row:
|
|
|
+ raise HTTPException(status_code=400, detail=f"人员不存在:{personnel_id}")
|
|
|
+ return row
|
|
|
+
|
|
|
+
|
|
|
+def _get_vehicle(db: Session, tenant_no: str, vehicle_id: str) -> PlVehicle:
|
|
|
+ row = (
|
|
|
+ db.query(PlVehicle)
|
|
|
+ .filter(
|
|
|
+ PlVehicle.id == vehicle_id,
|
|
|
+ PlVehicle.tenant_no == tenant_no,
|
|
|
+ PlVehicle.del_flag == 0,
|
|
|
+ )
|
|
|
+ .first()
|
|
|
+ )
|
|
|
+ if not row:
|
|
|
+ raise HTTPException(status_code=400, detail=f"车辆不存在:{vehicle_id}")
|
|
|
+ return row
|
|
|
+
|
|
|
+
|
|
|
+def _resolve_personnel(
|
|
|
+ db: Session,
|
|
|
+ tenant_no: str,
|
|
|
+ *,
|
|
|
+ key: str,
|
|
|
+ draft: PastePersonnelIn,
|
|
|
+ conflict: Optional[ConflictAction],
|
|
|
+ user_id: str,
|
|
|
+ dept_id: str,
|
|
|
+) -> str:
|
|
|
+ name = (draft.personnel_name or "").strip()
|
|
|
+ if not name:
|
|
|
+ raise HTTPException(status_code=400, detail=f"{key}姓名不能为空")
|
|
|
+
|
|
|
+ existing = _find_personnel_by_id_card(db, tenant_no, draft.id_card_no)
|
|
|
+ if existing and conflict is None:
|
|
|
+ raise HTTPException(
|
|
|
+ status_code=400,
|
|
|
+ detail=f"人员「{name}」身份证已存在,请先选择复用/覆盖/取消",
|
|
|
+ )
|
|
|
+ if conflict is not None:
|
|
|
+ if conflict.action == "cancel":
|
|
|
+ raise HTTPException(status_code=400, detail="已取消")
|
|
|
+ target = _get_personnel(db, tenant_no, conflict.existing_id)
|
|
|
+ if conflict.action == "reuse":
|
|
|
+ return target.id
|
|
|
+ # overwrite
|
|
|
+ target.personnel_name = name
|
|
|
+ target.phone_no = (draft.phone_no or "").strip() or None
|
|
|
+ card = (draft.id_card_no or "").strip().upper() or None
|
|
|
+ if card:
|
|
|
+ target.id_card_no = card
|
|
|
+ target.personnel_type = draft.personnel_type
|
|
|
+ target.personnel_affiliation = "carrier"
|
|
|
+ target.update_by = user_id
|
|
|
+ return target.id
|
|
|
+
|
|
|
+ personnel = PlPersonnel(
|
|
|
+ id=generate_uuid(),
|
|
|
+ tenant_no=tenant_no,
|
|
|
+ personnel_name=name,
|
|
|
+ phone_no=(draft.phone_no or "").strip() or None,
|
|
|
+ id_card_no=(draft.id_card_no or "").strip().upper() or None,
|
|
|
+ personnel_type=draft.personnel_type,
|
|
|
+ personnel_affiliation="carrier",
|
|
|
+ status="1",
|
|
|
+ create_by=user_id,
|
|
|
+ create_dept_id=dept_id,
|
|
|
+ )
|
|
|
+ db.add(personnel)
|
|
|
+ db.flush()
|
|
|
+ return personnel.id
|
|
|
+
|
|
|
+
|
|
|
+def _resolve_vehicle(
|
|
|
+ db: Session,
|
|
|
+ tenant_no: str,
|
|
|
+ *,
|
|
|
+ draft: PasteVehicleIn,
|
|
|
+ conflict: Optional[ConflictAction],
|
|
|
+ user_id: str,
|
|
|
+ dept_id: str,
|
|
|
+) -> str:
|
|
|
+ plate = (draft.license_plate or "").strip()
|
|
|
+ if not plate:
|
|
|
+ raise HTTPException(status_code=400, detail="车牌号不能为空")
|
|
|
+ carrier_id = (draft.carrier_id or "").strip()
|
|
|
+ if not carrier_id:
|
|
|
+ raise HTTPException(status_code=400, detail="请选择承运商")
|
|
|
+
|
|
|
+ carrier = (
|
|
|
+ db.query(PlCarrier)
|
|
|
+ .filter(
|
|
|
+ PlCarrier.id == carrier_id,
|
|
|
+ PlCarrier.tenant_no == tenant_no,
|
|
|
+ PlCarrier.del_flag == 0,
|
|
|
+ )
|
|
|
+ .first()
|
|
|
+ )
|
|
|
+ if not carrier:
|
|
|
+ raise HTTPException(status_code=400, detail="承运商不存在")
|
|
|
+
|
|
|
+ carrier_name = (draft.carrier_name or "").strip() or carrier.carrier_name
|
|
|
+ existing = _find_vehicle_by_plate(db, tenant_no, plate)
|
|
|
+ if existing and conflict is None:
|
|
|
+ raise HTTPException(
|
|
|
+ status_code=400,
|
|
|
+ detail=f"车牌「{plate}」已存在,请先选择复用/覆盖/取消",
|
|
|
+ )
|
|
|
+ if conflict is not None:
|
|
|
+ if conflict.action == "cancel":
|
|
|
+ raise HTTPException(status_code=400, detail="已取消")
|
|
|
+ target = _get_vehicle(db, tenant_no, conflict.existing_id)
|
|
|
+ if conflict.action == "reuse":
|
|
|
+ return target.id
|
|
|
+ target.trailer_no = (draft.trailer_no or "").strip() or None
|
|
|
+ target.load_capacity = draft.load_capacity
|
|
|
+ target.carrier_id = carrier_id
|
|
|
+ target.carrier_name = carrier_name
|
|
|
+ target.vehicle_type = "carrier"
|
|
|
+ target.update_by = user_id
|
|
|
+ return target.id
|
|
|
+
|
|
|
+ vehicle = PlVehicle(
|
|
|
+ id=generate_uuid(),
|
|
|
+ tenant_no=tenant_no,
|
|
|
+ vehicle_type="carrier",
|
|
|
+ carrier_id=carrier_id,
|
|
|
+ carrier_name=carrier_name,
|
|
|
+ license_plate=plate,
|
|
|
+ trailer_no=(draft.trailer_no or "").strip() or None,
|
|
|
+ load_capacity=draft.load_capacity,
|
|
|
+ vehicle_status="available",
|
|
|
+ create_by=user_id,
|
|
|
+ create_dept_id=dept_id,
|
|
|
+ )
|
|
|
+ db.add(vehicle)
|
|
|
+ db.flush()
|
|
|
+ return vehicle.id
|
|
|
+
|
|
|
+
|
|
|
+def commit_paste_recognize(
|
|
|
+ db: Session,
|
|
|
+ tenant_no: str,
|
|
|
+ body: PasteCommitBody,
|
|
|
+ *,
|
|
|
+ user_id: str,
|
|
|
+ dept_id: str,
|
|
|
+) -> PasteCommitResult:
|
|
|
+ cmap = _conflict_map(body.conflicts or [])
|
|
|
+ for item in cmap.values():
|
|
|
+ if item.action == "cancel":
|
|
|
+ raise HTTPException(status_code=400, detail="已取消")
|
|
|
+
|
|
|
+ need_vehicle = body.mode == "vehicle_bundle" or (
|
|
|
+ body.mode == "personnel_first" and body.create_vehicle
|
|
|
+ )
|
|
|
+ if need_vehicle and body.vehicle is None:
|
|
|
+ raise HTTPException(status_code=400, detail="缺少车辆信息")
|
|
|
+
|
|
|
+ driver_id: Optional[str] = None
|
|
|
+ escort_id: Optional[str] = None
|
|
|
+ vehicle_id: Optional[str] = None
|
|
|
+
|
|
|
+ if body.driver is not None:
|
|
|
+ driver_id = _resolve_personnel(
|
|
|
+ db,
|
|
|
+ tenant_no,
|
|
|
+ key="驾驶员",
|
|
|
+ draft=body.driver,
|
|
|
+ conflict=cmap.get("driver"),
|
|
|
+ user_id=user_id,
|
|
|
+ dept_id=dept_id,
|
|
|
+ )
|
|
|
+ if body.escort is not None:
|
|
|
+ escort_id = _resolve_personnel(
|
|
|
+ db,
|
|
|
+ tenant_no,
|
|
|
+ key="押运员",
|
|
|
+ draft=body.escort,
|
|
|
+ conflict=cmap.get("escort"),
|
|
|
+ user_id=user_id,
|
|
|
+ dept_id=dept_id,
|
|
|
+ )
|
|
|
+
|
|
|
+ if need_vehicle and body.vehicle is not None:
|
|
|
+ vehicle_id = _resolve_vehicle(
|
|
|
+ db,
|
|
|
+ tenant_no,
|
|
|
+ draft=body.vehicle,
|
|
|
+ conflict=cmap.get("vehicle"),
|
|
|
+ user_id=user_id,
|
|
|
+ dept_id=dept_id,
|
|
|
+ )
|
|
|
+ rows: list[VehiclePersonnelRow] = []
|
|
|
+ if driver_id and body.driver is not None:
|
|
|
+ rows.append(
|
|
|
+ VehiclePersonnelRow(
|
|
|
+ personnelId=driver_id,
|
|
|
+ personnelName=body.driver.personnel_name.strip(),
|
|
|
+ phoneNo=(body.driver.phone_no or "").strip() or None,
|
|
|
+ role="driver",
|
|
|
+ isDefault=1,
|
|
|
+ )
|
|
|
+ )
|
|
|
+ if escort_id and body.escort is not None:
|
|
|
+ rows.append(
|
|
|
+ VehiclePersonnelRow(
|
|
|
+ personnelId=escort_id,
|
|
|
+ personnelName=body.escort.personnel_name.strip(),
|
|
|
+ phoneNo=(body.escort.phone_no or "").strip() or None,
|
|
|
+ role="escort",
|
|
|
+ isDefault=0,
|
|
|
+ )
|
|
|
+ )
|
|
|
+ if rows:
|
|
|
+ save_vehicle_personnel(
|
|
|
+ db,
|
|
|
+ vehicle_id,
|
|
|
+ rows,
|
|
|
+ tenant_no,
|
|
|
+ user_id=user_id,
|
|
|
+ dept_id=dept_id,
|
|
|
+ vehicle_type="carrier",
|
|
|
+ )
|
|
|
+
|
|
|
+ if body.mode == "personnel_first" and not need_vehicle:
|
|
|
+ if not driver_id and not escort_id:
|
|
|
+ raise HTTPException(status_code=400, detail="请至少识别一名人员")
|
|
|
+
|
|
|
+ if body.mode == "vehicle_bundle" and not vehicle_id:
|
|
|
+ raise HTTPException(status_code=400, detail="车辆创建失败")
|
|
|
+
|
|
|
+ return PasteCommitResult(
|
|
|
+ vehicleId=vehicle_id,
|
|
|
+ driverId=driver_id,
|
|
|
+ escortId=escort_id,
|
|
|
+ )
|