|
|
@@ -1,9 +1,10 @@
|
|
|
"""运输计划管理 - 计划 API"""
|
|
|
import re
|
|
|
-from datetime import date
|
|
|
-from typing import Optional
|
|
|
+from datetime import date, datetime
|
|
|
+from typing import List, Optional
|
|
|
|
|
|
from fastapi import APIRouter, Body, Depends, HTTPException, Query, status
|
|
|
+from pydantic import BaseModel, Field
|
|
|
from sqlalchemy import func, or_
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
@@ -14,6 +15,13 @@ from app.core.delete_guard import assert_row_deletable_by_creator, user_info_fro
|
|
|
from app.core.menu_guard import SUPER_ADMIN_ROLE, get_user_menu_codes
|
|
|
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.utils.excel_export import (
|
|
|
+ EXPORT_MAX_ROWS,
|
|
|
+ ExportColumn,
|
|
|
+ ExportValidationError,
|
|
|
+ export_rows_to_xlsx_response,
|
|
|
+ filter_export_columns,
|
|
|
+)
|
|
|
from app.core.utils import generate_uuid
|
|
|
from app.models.crm_models import CrmContract, CrmContractProduct, CrmProduct
|
|
|
from app.models.db_models import DbInboundOutbound
|
|
|
@@ -465,6 +473,122 @@ def list_plans(
|
|
|
return PlanListResponse(total=total, list=result_list)
|
|
|
|
|
|
|
|
|
+
|
|
|
+PLAN_EXPORT_COLUMNS = [
|
|
|
+ ExportColumn(title="计划编号", field="planNo"),
|
|
|
+ ExportColumn(title="跟进状态", field="followStatus"),
|
|
|
+ ExportColumn(title="计划日期", field="planDate"),
|
|
|
+ ExportColumn(title="单据类型", field="planType"),
|
|
|
+ ExportColumn(title="供方", field="supplierName"),
|
|
|
+ ExportColumn(title="需方", field="customerName"),
|
|
|
+ ExportColumn(title="车牌号", field="licensePlate"),
|
|
|
+ ExportColumn(title="司机", field="driverName"),
|
|
|
+ ExportColumn(title="运输状态", field="transportStatus"),
|
|
|
+ ExportColumn(title="归档状态", field="archiveStatus"),
|
|
|
+ ExportColumn(title="备注", field="remark"),
|
|
|
+]
|
|
|
+
|
|
|
+
|
|
|
+class PlanExportBody(BaseModel):
|
|
|
+ q: Optional[str] = None
|
|
|
+ plan_no: Optional[str] = Field(default=None, alias="planNo")
|
|
|
+ plan_type: Optional[str] = Field(default=None, alias="planType")
|
|
|
+ follow_status: Optional[str] = Field(default=None, alias="followStatus")
|
|
|
+ transport_status: Optional[str] = Field(default=None, alias="transportStatus")
|
|
|
+ archive_status: Optional[str] = Field(default=None, alias="archiveStatus")
|
|
|
+ customer_name: Optional[str] = Field(default=None, alias="customerName")
|
|
|
+ product_name: Optional[str] = Field(default=None, alias="productName")
|
|
|
+ license_plate: Optional[str] = Field(default=None, alias="licensePlate")
|
|
|
+ plan_date_from: Optional[date] = Field(default=None, alias="planDateFrom")
|
|
|
+ plan_date_to: Optional[date] = Field(default=None, alias="planDateTo")
|
|
|
+ has_hazard_product: Optional[int] = Field(default=None, alias="hasHazardProduct")
|
|
|
+ cert_upload_status: Optional[str] = Field(default=None, alias="certUploadStatus")
|
|
|
+ ids: Optional[List[str]] = None
|
|
|
+ columns: Optional[List[str]] = None
|
|
|
+
|
|
|
+ model_config = {"populate_by_name": True}
|
|
|
+
|
|
|
+
|
|
|
+@router.post("/export")
|
|
|
+def export_plans(
|
|
|
+ body: PlanExportBody,
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+ user: DbUserContext = Depends(get_db_current_user),
|
|
|
+):
|
|
|
+ """导出计划列表(当前筛选全量,或 ids 已选)。"""
|
|
|
+ try:
|
|
|
+ tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
|
|
+ query = db.query(PlPlan).filter(
|
|
|
+ PlPlan.tenant_no == tenant_no,
|
|
|
+ PlPlan.del_flag == 0,
|
|
|
+ )
|
|
|
+ if body.q:
|
|
|
+ q_kw = body.q.strip()
|
|
|
+ if q_kw:
|
|
|
+ q_lower = q_kw.lower()
|
|
|
+ query = query.filter(
|
|
|
+ or_(
|
|
|
+ PlPlan.plan_no.contains(q_kw),
|
|
|
+ func.lower(PlPlan.supplier_name).contains(q_lower),
|
|
|
+ func.lower(PlPlan.customer_name).contains(q_lower),
|
|
|
+ func.lower(PlPlan.destination_name).contains(q_lower),
|
|
|
+ _plan_product_name_exists(db, tenant_no, q_kw),
|
|
|
+ _plan_plate_match(q_kw),
|
|
|
+ )
|
|
|
+ )
|
|
|
+ if body.plan_no:
|
|
|
+ query = query.filter(PlPlan.plan_no.contains(body.plan_no))
|
|
|
+ if body.plan_type:
|
|
|
+ query = query.filter(PlPlan.plan_type == body.plan_type)
|
|
|
+ if body.follow_status:
|
|
|
+ query = query.filter(PlPlan.follow_status == body.follow_status)
|
|
|
+ if body.transport_status:
|
|
|
+ query = query.filter(PlPlan.transport_status == body.transport_status)
|
|
|
+ if body.archive_status:
|
|
|
+ query = query.filter(PlPlan.archive_status == body.archive_status)
|
|
|
+ if body.customer_name:
|
|
|
+ query = query.filter(func.lower(PlPlan.customer_name).contains(body.customer_name.lower()))
|
|
|
+ if body.product_name:
|
|
|
+ product_name_kw = body.product_name.strip()
|
|
|
+ if product_name_kw:
|
|
|
+ query = query.filter(_plan_product_name_exists(db, tenant_no, product_name_kw))
|
|
|
+ if body.license_plate:
|
|
|
+ plate_kw = body.license_plate.strip()
|
|
|
+ if plate_kw:
|
|
|
+ query = query.filter(_plan_plate_match(plate_kw))
|
|
|
+ if body.plan_date_from:
|
|
|
+ query = query.filter(PlPlan.plan_date >= body.plan_date_from)
|
|
|
+ if body.plan_date_to:
|
|
|
+ query = query.filter(PlPlan.plan_date <= body.plan_date_to)
|
|
|
+ if body.has_hazard_product == 1:
|
|
|
+ query = query.filter(plan_has_hazard_product_exists(tenant_no))
|
|
|
+ if (body.cert_upload_status or "").strip().lower() == "incomplete":
|
|
|
+ query = query.filter(plan_has_incomplete_hazard_cert_exists(tenant_no))
|
|
|
+ if body.ids:
|
|
|
+ query = query.filter(PlPlan.id.in_(body.ids))
|
|
|
+ query = apply_pl_scope(query, PlPlan, user, db, tenant_no)
|
|
|
+ plans = query.order_by(PlPlan.create_time.desc()).limit(EXPORT_MAX_ROWS + 1).all()
|
|
|
+ items = []
|
|
|
+ for plan in plans:
|
|
|
+ data = PlanWithProducts.model_validate(plan).model_dump(by_alias=True)
|
|
|
+ # flatten date
|
|
|
+ if data.get("planDate") is not None:
|
|
|
+ data["planDate"] = str(data["planDate"])[:10]
|
|
|
+ items.append(data)
|
|
|
+ cols = filter_export_columns(PLAN_EXPORT_COLUMNS, body.columns)
|
|
|
+ date_str = datetime.now().strftime("%Y%m%d")
|
|
|
+ return export_rows_to_xlsx_response(
|
|
|
+ db,
|
|
|
+ tenant_no=tenant_no,
|
|
|
+ sheet_title="计划列表",
|
|
|
+ columns=cols,
|
|
|
+ rows=items,
|
|
|
+ filename_stem=f"计划列表_{date_str}",
|
|
|
+ )
|
|
|
+ except ExportValidationError as e:
|
|
|
+ raise HTTPException(status_code=400, detail=str(e)) from e
|
|
|
+
|
|
|
+
|
|
|
@router.get("/count-by-contract")
|
|
|
def count_plans_by_contract(
|
|
|
contract_ids: str = Query(..., alias="contractIds", description="合同ID,逗号分隔"),
|