| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901 |
- """运输计划管理 - 运输 API"""
- from datetime import date, datetime
- from typing import Optional
- import json
- import secrets
- from fastapi import APIRouter, Depends, HTTPException, Query, status
- from pydantic import BaseModel, Field
- from sqlalchemy import func, or_
- from sqlalchemy.orm import Session
- from app.core.database import get_db
- from app.core.db_auth import DbUserContext, get_db_current_user
- from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope
- from app.core.tenant_context import get_tenant_no
- from app.core.utils import generate_uuid
- from app.core.config import settings
- from app.models.pl_models import (
- PlCarrier,
- PlDriver,
- PlPersonnel,
- PlPlan,
- PlPlanProduct,
- PlPlanTransport,
- PlTransport,
- PlTransportApproval,
- PlTransportDbLink,
- PlVehicle,
- )
- from app.schemas.pl.transport import (
- ApprovalRecordRead,
- ApprovalRequest,
- ApprovalSubmit,
- TransportArrange,
- TransportArrangeResult,
- TransportCreate,
- TransportFeedback,
- TransportInventoryLinkRead,
- TransportSiteLinkRead,
- TransportListQuery,
- TransportListResponse,
- TransportRead,
- )
- from app.services.pl_dibang_bridge_service import (
- revoke_regeneratable_dibang_for_transport,
- sync_dibang_for_transport,
- )
- from app.services.pl_inventory_link_read import build_transport_inventory_link_read
- from app.services.pl_site_link_read import build_transport_site_link_read
- from app.services.pl_transport_site_service import (
- revoke_site_links_for_transport,
- sync_site_links_for_transport,
- )
- from app.services.pl_plan_service import is_ton_product_unit
- router = APIRouter(prefix="/order", tags=["运输计划-运输管理"])
- def _generate_transport_no(db: Session) -> str:
- """生成运输编号 SXYS######"""
- now = date.today()
- prefix = f"SXYS{now.strftime('%Y%m')}"
- last = (
- db.query(PlTransport)
- .filter(
- PlTransport.transport_no.like(f"{prefix}%"),
- PlTransport.del_flag == 0,
- )
- .order_by(PlTransport.transport_no.desc())
- .first()
- )
- if last:
- last_seq = int(last.transport_no[-6:])
- seq = last_seq + 1
- else:
- seq = 1
- return f"{prefix}{seq:06d}"
- def _resolve_driver_info(db: Session, tenant_no: str, driver_id: str) -> Optional[dict]:
- """解析司机信息:优先 pl_personnel,兼容旧 pl_driver。"""
- personnel = (
- db.query(PlPersonnel)
- .filter(
- PlPersonnel.id == driver_id,
- PlPersonnel.tenant_no == tenant_no,
- PlPersonnel.del_flag == 0,
- )
- .first()
- )
- if personnel:
- return {
- "id": personnel.id,
- "name": personnel.personnel_name,
- "phone": personnel.phone_no,
- }
- legacy = (
- db.query(PlDriver)
- .filter(
- PlDriver.id == driver_id,
- PlDriver.tenant_no == tenant_no,
- PlDriver.del_flag == 0,
- )
- .first()
- )
- if legacy:
- return {
- "id": legacy.id,
- "name": legacy.driver_name,
- "phone": legacy.phone_no,
- }
- return None
- def _get_vehicle_info(db: Session, vehicle_id: str, tenant_no: Optional[str] = None) -> dict:
- vehicle = db.query(PlVehicle).filter(PlVehicle.id == vehicle_id).first()
- if not vehicle:
- return {}
- info = {
- "license_plate": vehicle.license_plate,
- "carrier_id": vehicle.carrier_id,
- "carrier_name": vehicle.carrier_name,
- }
- if vehicle.default_driver_id:
- driver_info = (
- _resolve_driver_info(db, tenant_no, vehicle.default_driver_id)
- if tenant_no
- else None
- )
- if not driver_info:
- personnel = db.query(PlPersonnel).filter(
- PlPersonnel.id == vehicle.default_driver_id,
- PlPersonnel.del_flag == 0,
- ).first()
- if personnel:
- driver_info = {
- "id": personnel.id,
- "name": personnel.personnel_name,
- "phone": personnel.phone_no,
- }
- else:
- legacy = db.query(PlDriver).filter(
- PlDriver.id == vehicle.default_driver_id,
- PlDriver.del_flag == 0,
- ).first()
- if legacy:
- driver_info = {
- "id": legacy.id,
- "name": legacy.driver_name,
- "phone": legacy.phone_no,
- }
- if driver_info:
- info["driver_id"] = driver_info["id"]
- info["driver_name"] = driver_info["name"]
- info["driver_phone"] = driver_info["phone"]
- elif vehicle.default_driver_name:
- info["driver_id"] = vehicle.default_driver_id
- info["driver_name"] = vehicle.default_driver_name
- return info
- def _ensure_driver_access_token(transport: PlTransport) -> str:
- token = (transport.driver_access_token or "").strip()
- if not token:
- token = secrets.token_urlsafe(32)[:64]
- transport.driver_access_token = token
- return token
- def _rotate_driver_access_token(transport: PlTransport) -> str:
- token = secrets.token_urlsafe(32)[:64]
- transport.driver_access_token = token
- return token
- def _is_rearrange(plan: PlPlan, existing_transport: Optional[PlTransport]) -> bool:
- if plan.transport_status == "arranged":
- return True
- if existing_transport and existing_transport.transport_status != "cancelled":
- return True
- return False
- def _transport_has_db_links(db: Session, tenant_no: str, transport_id: str) -> bool:
- return (
- db.query(PlTransportDbLink.id)
- .filter(
- PlTransportDbLink.tenant_no == tenant_no,
- PlTransportDbLink.transport_id == transport_id,
- PlTransportDbLink.del_flag == 0,
- )
- .first()
- is not None
- )
- def _apply_plan_logistics_from_payload(plan: PlPlan, payload: TransportArrange) -> None:
- if payload.pickup_address is not None:
- plan.pickup_address = payload.pickup_address
- elif payload.start_point is not None:
- plan.pickup_address = payload.start_point
- if payload.pickup_time is not None:
- plan.pickup_time = payload.pickup_time
- if payload.pickup_notes is not None:
- plan.pickup_notes = payload.pickup_notes
- if payload.destination_address is not None:
- plan.destination_address = payload.destination_address
- elif payload.end_point is not None:
- plan.destination_address = payload.end_point
- if payload.delivery_time is not None:
- plan.delivery_time = payload.delivery_time
- if payload.destination_notes is not None:
- plan.destination_notes = payload.destination_notes
- def _resolve_transport_endpoints(plan: PlPlan, payload: TransportArrange) -> tuple[Optional[str], Optional[str]]:
- start_point = (
- payload.pickup_address
- or payload.start_point
- or plan.pickup_address
- )
- end_point = (
- payload.destination_address
- or payload.end_point
- or plan.destination_address
- or plan.destination_name
- )
- return start_point, end_point
- def _apply_transport_arrange_fields(
- transport: PlTransport,
- *,
- payload: TransportArrange,
- vehicle_info: dict,
- driver_info: dict,
- carrier_name: Optional[str],
- start_point: Optional[str],
- end_point: Optional[str],
- freight_amount: Optional[float],
- user_id: str,
- ) -> None:
- transport.vehicle_id = payload.vehicle_id
- transport.license_plate = vehicle_info.get("license_plate")
- transport.driver_id = driver_info["id"]
- transport.driver_name = driver_info["name"]
- transport.driver_phone = driver_info["phone"]
- transport.carrier_id = payload.carrier_id or vehicle_info.get("carrier_id")
- transport.carrier_name = carrier_name
- transport.transport_medium = payload.transport_medium
- transport.start_point = start_point
- transport.end_point = end_point
- transport.distance_km = payload.distance_km
- transport.unit = payload.unit or ("吨" if payload.freight_type == "by_weight" else "趟")
- transport.freight_price = payload.freight_price
- transport.freight_amount = freight_amount or 0
- transport.freight_type = payload.freight_type
- transport.has_invoice = payload.has_invoice
- if payload.remark is not None:
- transport.remark = payload.remark
- transport.update_by = user_id
- def _build_driver_url(token: str) -> str:
- origin = (settings.PUBLIC_WEB_ORIGIN or "").rstrip("/")
- return f"{origin}/#/public/pl/transport/{token}"
- def _build_arrange_result(
- transport: PlTransport,
- site_records: list[TransportSiteLinkRead],
- inventory_records: list[TransportInventoryLinkRead],
- ) -> TransportArrangeResult:
- token = transport.driver_access_token or ""
- result = TransportArrangeResult.model_validate(transport)
- result.driver_access_token = token
- result.driver_url = _build_driver_url(token) if token else None
- result.site_records = site_records
- result.inventory_records = inventory_records
- return result
- # =============================================================================
- # 运输列表
- # =============================================================================
- @router.get("/", response_model=TransportListResponse)
- def list_transports(
- q: Optional[str] = Query(default=None, description="模糊搜索"),
- transport_no: Optional[str] = Query(default=None, alias="transportNo", description="运输编号"),
- transport_status: Optional[str] = Query(default=None, alias="transportStatus", description="运输状态"),
- approval_status: Optional[str] = Query(default=None, alias="approvalStatus", description="审批状态"),
- vehicle_id: Optional[str] = Query(default=None, alias="vehicleId", description="车辆ID"),
- driver_id: Optional[str] = Query(default=None, alias="driverId", description="司机ID"),
- page: int = Query(default=1, ge=1),
- page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
- db: Session = Depends(get_db),
- user: DbUserContext = Depends(get_db_current_user),
- ) -> TransportListResponse:
- tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
- query = db.query(PlTransport).filter(
- PlTransport.tenant_no == tenant_no,
- PlTransport.del_flag == 0,
- )
- if q:
- query = query.filter(
- or_(
- PlTransport.transport_no.contains(q),
- PlTransport.license_plate.contains(q),
- PlTransport.driver_name.contains(q),
- )
- )
- if transport_no:
- query = query.filter(PlTransport.transport_no.contains(transport_no))
- if transport_status:
- query = query.filter(PlTransport.transport_status == transport_status)
- if approval_status:
- query = query.filter(PlTransport.approval_status == approval_status)
- if vehicle_id:
- query = query.filter(PlTransport.vehicle_id == vehicle_id)
- if driver_id:
- query = query.filter(PlTransport.driver_id == driver_id)
- query = apply_pl_scope(query, PlTransport, user, db, tenant_no)
- total = query.count()
- offset = (page - 1) * page_size
- transports = query.order_by(PlTransport.create_time.desc()).offset(offset).limit(page_size).all()
- return TransportListResponse(total=total, list=transports)
- # =============================================================================
- # 创建运输
- # =============================================================================
- @router.post("/", response_model=TransportRead, status_code=status.HTTP_201_CREATED)
- def create_transport(
- payload: TransportCreate,
- db: Session = Depends(get_db),
- user: DbUserContext = Depends(get_db_current_user),
- ) -> TransportRead:
- tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
- transport_no = _generate_transport_no(db)
- transport = PlTransport(
- id=generate_uuid(),
- tenant_no=tenant_no,
- transport_no=transport_no,
- vehicle_id=payload.vehicle_id,
- license_plate=payload.license_plate,
- driver_id=payload.driver_id,
- driver_name=payload.driver_name,
- driver_phone=payload.driver_phone,
- carrier_id=payload.carrier_id,
- carrier_name=payload.carrier_name,
- transport_medium=payload.transport_medium,
- start_point=payload.start_point,
- end_point=payload.end_point,
- distance_km=payload.distance_km,
- unit=payload.unit,
- freight_price=payload.freight_price,
- freight_amount=payload.freight_amount,
- freight_type=payload.freight_type,
- has_invoice=payload.has_invoice,
- qr_expire_date=payload.qr_expire_date,
- transport_status=payload.transport_status or "draft",
- approval_status=payload.approval_status or "none",
- remark=payload.remark,
- create_by=user.id,
- create_dept_id=user.dept_id,
- )
- db.add(transport)
- # 关联计划
- if payload.plan_ids:
- for pid in payload.plan_ids:
- pt = PlPlanTransport(
- id=generate_uuid(),
- tenant_no=tenant_no,
- plan_id=pid,
- transport_id=transport.id,
- create_by=user.id,
- create_dept_id=user.dept_id,
- )
- db.add(pt)
- # 更新计划关联的运输信息
- plan = db.query(PlPlan).filter(PlPlan.id == pid, PlPlan.del_flag == 0).first()
- if plan:
- assert_pl_row_in_scope(db, plan, PlPlan, user, tenant_no)
- plan.transport_id = transport.id
- plan.transport_no = transport_no
- plan.vehicle_id = transport.vehicle_id
- plan.license_plate = transport.license_plate
- plan.driver_id = transport.driver_id
- plan.driver_name = transport.driver_name
- db.commit()
- db.refresh(transport)
- return transport
- # =============================================================================
- # 运输安排
- # =============================================================================
- @router.post("/arrange/{plan_id}", response_model=TransportArrangeResult, status_code=status.HTTP_201_CREATED)
- def arrange_transport(
- plan_id: str,
- payload: TransportArrange,
- db: Session = Depends(get_db),
- user: DbUserContext = Depends(get_db_current_user),
- ) -> TransportArrangeResult:
- tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
- # 获取计划
- plan = db.query(PlPlan).filter(
- PlPlan.id == plan_id,
- PlPlan.tenant_no == tenant_no,
- PlPlan.del_flag == 0,
- ).first()
- if not plan:
- raise HTTPException(status_code=404, detail="计划不存在")
- assert_pl_row_in_scope(db, plan, PlPlan, user, tenant_no)
- # 获取车辆信息
- vehicle_info = _get_vehicle_info(db, payload.vehicle_id, tenant_no)
- # 获取司机信息(driverId 为人员 pl_personnel.id)
- driver_info = _resolve_driver_info(db, tenant_no, payload.driver_id)
- if not driver_info:
- raise HTTPException(status_code=400, detail="司机不存在")
- # 检查产品属性(易制毒/易制爆需要证件)
- products = db.query(PlPlanProduct).filter(
- PlPlanProduct.plan_id == plan_id,
- PlPlanProduct.del_flag == 0,
- ).all()
- for prod in products:
- if prod.is_explosive or prod.is_toxic:
- if not prod.purchase_cert_url or not prod.transport_cert_url:
- raise HTTPException(
- status_code=400,
- detail=f"产品【{prod.product_name}】为{'易制爆' if prod.is_explosive else '易制毒'},必须上传购买证和运输证"
- )
- # 计算运费
- freight_amount = payload.freight_amount
- if payload.freight_type == "by_weight" and payload.freight_price:
- total_qty = sum(
- float(p.quantity or p.inbound_estimate_qty or p.outbound_estimate_qty or 0)
- for p in products
- )
- freight_amount = total_qty * float(payload.freight_price)
- carrier_name = vehicle_info.get("carrier_name")
- if payload.carrier_id:
- carrier = db.query(PlCarrier).filter(PlCarrier.id == payload.carrier_id).first()
- if carrier:
- carrier_name = carrier.carrier_name
- _apply_plan_logistics_from_payload(plan, payload)
- start_point, end_point = _resolve_transport_endpoints(plan, payload)
- existing_transport = None
- if plan.transport_id:
- existing_transport = db.query(PlTransport).filter(
- PlTransport.id == plan.transport_id,
- PlTransport.tenant_no == tenant_no,
- PlTransport.del_flag == 0,
- ).first()
- is_rearrange = _is_rearrange(plan, existing_transport)
- if (
- plan.plan_type in ("inbound", "outbound")
- and is_rearrange
- and existing_transport
- and _transport_has_db_links(db, tenant_no, existing_transport.id)
- ):
- try:
- revoke_regeneratable_dibang_for_transport(
- db,
- tenant_no=tenant_no,
- transport_id=existing_transport.id,
- user_id=user.id,
- )
- except ValueError as exc:
- raise HTTPException(status_code=400, detail=str(exc)) from exc
- if existing_transport and existing_transport.transport_status != "cancelled":
- transport = existing_transport
- _apply_transport_arrange_fields(
- transport,
- payload=payload,
- vehicle_info=vehicle_info,
- driver_info=driver_info,
- carrier_name=carrier_name,
- start_point=start_point,
- end_point=end_point,
- freight_amount=freight_amount,
- user_id=user.id,
- )
- transport_no = transport.transport_no
- else:
- transport_no = _generate_transport_no(db)
- transport = PlTransport(
- id=generate_uuid(),
- tenant_no=tenant_no,
- transport_no=transport_no,
- transport_status="draft",
- approval_status="none",
- create_by=user.id,
- create_dept_id=user.dept_id,
- )
- db.add(transport)
- _apply_transport_arrange_fields(
- transport,
- payload=payload,
- vehicle_info=vehicle_info,
- driver_info=driver_info,
- carrier_name=carrier_name,
- start_point=start_point,
- end_point=end_point,
- freight_amount=freight_amount,
- user_id=user.id,
- )
- pt = PlPlanTransport(
- id=generate_uuid(),
- tenant_no=tenant_no,
- plan_id=plan_id,
- transport_id=transport.id,
- create_by=user.id,
- create_dept_id=user.dept_id,
- )
- db.add(pt)
- plan.transport_id = transport.id
- plan.transport_no = transport_no
- plan.vehicle_id = transport.vehicle_id
- plan.license_plate = transport.license_plate
- plan.driver_id = transport.driver_id
- plan.driver_name = transport.driver_name
- plan.transport_status = "arranged"
- plan.freight_amount = freight_amount or 0
- plan.update_by = user.id
- if is_rearrange:
- _rotate_driver_access_token(transport)
- else:
- _ensure_driver_access_token(transport)
- site_records: list[TransportSiteLinkRead] = []
- inventory_records: list[TransportInventoryLinkRead] = []
- if plan.plan_type in ("inbound", "outbound"):
- if is_rearrange and existing_transport:
- revoke_site_links_for_transport(
- db,
- tenant_no=tenant_no,
- transport_id=transport.id,
- user_id=user.id,
- )
- try:
- site_link_results = sync_site_links_for_transport(
- db,
- tenant_no=tenant_no,
- plan=plan,
- products=products,
- transport=transport,
- user_id=user.id,
- dept_id=user.dept_id,
- )
- site_records = [
- build_transport_site_link_read(
- db,
- plan_product_id=item.plan_product_id,
- product_name=item.product_name,
- site_link_id=item.site_link_id,
- plan_no=item.plan_no,
- business_type=item.business_type,
- )
- for item in site_link_results
- ]
- except ValueError as exc:
- db.rollback()
- raise HTTPException(status_code=400, detail=str(exc)) from exc
- if plan.plan_type in ("inbound", "outbound") and payload.generate_dibang_plan:
- non_ton_products = [
- prod.product_name or prod.product_no or "未知产品"
- for prod in products
- if not is_ton_product_unit(prod.unit)
- ]
- if non_ton_products:
- raise HTTPException(
- status_code=400,
- detail=f"产品【{non_ton_products[0]}】等单位非吨,不支持生成地磅单",
- )
- try:
- link_results = sync_dibang_for_transport(
- db,
- tenant_no=tenant_no,
- plan=plan,
- products=products,
- transport=transport,
- user_id=user.id,
- dept_id=user.dept_id,
- )
- inventory_records = [
- build_transport_inventory_link_read(
- db,
- plan_product_id=item.plan_product_id,
- product_name=item.product_name,
- db_purchase_plan_id=item.db_purchase_plan_id,
- db_inventory_id=item.db_inventory_id,
- batch_no=item.batch_no,
- plan_no=item.plan_no,
- )
- for item in link_results
- ]
- except ValueError as exc:
- db.rollback()
- raise HTTPException(status_code=400, detail=str(exc)) from exc
- db.commit()
- db.refresh(transport)
- return _build_arrange_result(transport, site_records, inventory_records)
- # =============================================================================
- # 获取运输详情
- # =============================================================================
- @router.get("/{id}", response_model=TransportRead)
- def get_transport(
- id: str,
- db: Session = Depends(get_db),
- user: DbUserContext = Depends(get_db_current_user),
- ) -> TransportRead:
- tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
- transport = _get_transport(db, tenant_no, id, user)
- return transport
- # =============================================================================
- # 生成二维码
- # =============================================================================
- class QrCodeResponse(BaseModel):
- transport_id: str = Field(alias="transportId")
- transport_no: str = Field(alias="transportNo")
- qr_code: str = Field(alias="qrCode")
- expire_date: Optional[date] = Field(default=None, alias="expireDate")
- model_config = {"from_attributes": True, "populate_by_name": True}
- @router.get("/{id}/qrcode", response_model=QrCodeResponse)
- def generate_qrcode(
- id: str,
- expire_days: int = Query(default=30, ge=1, le=365, description="有效期天数"),
- db: Session = Depends(get_db),
- user: DbUserContext = Depends(get_db_current_user),
- ) -> QrCodeResponse:
- tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
- transport = _get_transport(db, tenant_no, id, user)
- from datetime import timedelta
- expire_date = date.today() + timedelta(days=expire_days)
- # 生成二维码内容(URL scheme)
- qr_content = f"/transport/scan/{transport.id}?code={transport.transport_no}"
- transport.qr_code = qr_content
- transport.qr_expire_date = expire_date
- db.commit()
- return QrCodeResponse(
- transport_id=transport.id,
- transport_no=transport.transport_no,
- qr_code=qr_content,
- expire_date=expire_date,
- )
- # =============================================================================
- # 司机扫码回传(无需登录)
- # =============================================================================
- @router.post("/scan/feedback")
- def transport_scan_feedback(
- payload: TransportFeedback,
- db: Session = Depends(get_db),
- ) -> dict:
- """司机扫码回传,无需登录"""
- transport = (
- db.query(PlTransport)
- .filter(
- PlTransport.transport_no == payload.transport_no,
- PlTransport.del_flag == 0,
- )
- .first()
- )
- if not transport:
- raise HTTPException(status_code=404, detail="运输单不存在")
- if payload.verify_code and payload.verify_code != transport.transport_no:
- raise HTTPException(status_code=400, detail="验证码错误")
- # 更新回传信息
- if payload.feedback_qty:
- transport.feedback_qty = payload.feedback_qty
- if payload.feedback_images:
- transport.feedback_images = json.dumps(payload.feedback_images)
- if payload.feedback_remark:
- transport.feedback_remark = payload.feedback_remark
- transport.feedback_time = datetime.now()
- db.commit()
- return {"success": True, "message": "回传成功"}
- # =============================================================================
- # 提交审批
- # =============================================================================
- @router.post("/{id}/submit-approval", response_model=TransportRead)
- def submit_approval(
- id: str,
- payload: ApprovalSubmit,
- db: Session = Depends(get_db),
- user: DbUserContext = Depends(get_db_current_user),
- ) -> TransportRead:
- tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
- transport = _get_transport(db, tenant_no, id, user)
- if transport.approval_status not in ("none", "rejected"):
- raise HTTPException(status_code=400, detail="当前状态不允许提交审批")
- # 创建审批记录
- for idx, approver_id in enumerate(payload.approver_ids):
- from app.models.models import SysUser
- approver = db.query(SysUser).filter(SysUser.id == approver_id).first()
- if not approver:
- raise HTTPException(status_code=400, detail=f"审批人不存在: {approver_id}")
- approval = PlTransportApproval(
- id=generate_uuid(),
- tenant_no=tenant_no,
- transport_id=id,
- approver_id=approver_id,
- approver_name=approver.nick_name,
- approval_order=idx + 1,
- status="pending",
- create_by=user.id,
- create_dept_id=user.dept_id,
- )
- db.add(approval)
- transport.approval_status = "pending"
- db.commit()
- db.refresh(transport)
- return transport
- # =============================================================================
- # 审批
- # =============================================================================
- @router.post("/approval/{id}", response_model=ApprovalRecordRead)
- def process_approval(
- id: str,
- payload: ApprovalRequest,
- db: Session = Depends(get_db),
- user: DbUserContext = Depends(get_db_current_user),
- ) -> ApprovalRecordRead:
- tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
- approval = (
- db.query(PlTransportApproval)
- .filter(
- PlTransportApproval.id == id,
- PlTransportApproval.tenant_no == tenant_no,
- PlTransportApproval.del_flag == 0,
- )
- .first()
- )
- if not approval:
- raise HTTPException(status_code=404, detail="审批记录不存在")
- if approval.status != "pending":
- raise HTTPException(status_code=400, detail="该审批已处理")
- # 更新审批状态
- from datetime import datetime
- approval.status = "approved" if payload.action == "approve" else "rejected"
- approval.comment = payload.comment
- approval.approval_time = datetime.now()
- # 更新运输审批状态
- transport = db.query(PlTransport).filter(PlTransport.id == approval.transport_id).first()
- if transport:
- if payload.action == "approve":
- # 检查是否所有审批人都通过了
- pending = (
- db.query(PlTransportApproval)
- .filter(
- PlTransportApproval.transport_id == approval.transport_id,
- PlTransportApproval.status == "pending",
- PlTransportApproval.del_flag == 0,
- )
- .count()
- )
- if pending == 0:
- transport.approval_status = "approved"
- transport.transport_status = "pending"
- else:
- transport.approval_status = "rejected"
- transport.transport_status = "draft"
- db.commit()
- db.refresh(approval)
- return approval
- # =============================================================================
- # 获取运输的审批记录
- # =============================================================================
- @router.get("/{id}/approvals", response_model=list[ApprovalRecordRead])
- def get_transport_approvals(
- id: str,
- db: Session = Depends(get_db),
- user: DbUserContext = Depends(get_db_current_user),
- ) -> list[ApprovalRecordRead]:
- tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
- approvals = (
- db.query(PlTransportApproval)
- .filter(
- PlTransportApproval.transport_id == id,
- PlTransportApproval.tenant_no == tenant_no,
- PlTransportApproval.del_flag == 0,
- )
- .order_by(PlTransportApproval.approval_order)
- .all()
- )
- return approvals
- # =============================================================================
- # 辅助函数
- # =============================================================================
- def _get_transport(db: Session, tenant_no: str, transport_id: str, user: DbUserContext) -> PlTransport:
- transport = (
- db.query(PlTransport)
- .filter(
- PlTransport.id == transport_id,
- PlTransport.tenant_no == tenant_no,
- PlTransport.del_flag == 0,
- )
- .first()
- )
- if not transport:
- raise HTTPException(status_code=404, detail="运输不存在")
- assert_pl_row_in_scope(db, transport, PlTransport, user, tenant_no)
- return transport
|