Просмотр исходного кода

feat(pl): show product and plan type columns on contract completion list

Align the合同完善 table with运输计划 by returning products/planType from contract-gaps and rendering them via PlPlanListProductCell.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 месяцев назад
Родитель
Сommit
09a57a7af9

+ 2 - 0
backend/app/schemas/pl/plan.py

@@ -430,6 +430,7 @@ class PlanContractGapRead(BaseModel):
     id: str
     plan_no: str = Field(..., alias="planNo")
     plan_date: Optional[date] = Field(None, alias="planDate")
+    plan_type: Optional[str] = Field(None, alias="planType")
     follow_status: str = Field(..., alias="followStatus")
     supplier_id: Optional[str] = Field(None, alias="supplierId")
     supplier_name: Optional[str] = Field(None, alias="supplierName")
@@ -440,6 +441,7 @@ class PlanContractGapRead(BaseModel):
     demander_contract_exempt: bool = Field(..., alias="demanderContractExempt")
     supplier_contracts: list[PlanContractLink] = Field(default_factory=list, alias="supplierContracts")
     demander_contracts: list[PlanContractLink] = Field(default_factory=list, alias="demanderContracts")
+    products: list[PlanProductRead] = Field(default_factory=list, description="产品明细")
     model_config = {"populate_by_name": True, "from_attributes": True}
 
 

+ 30 - 2
backend/app/services/pl_plan_contract_gap.py

@@ -7,8 +7,8 @@ from fastapi import HTTPException
 from sqlalchemy import and_, exists, func, or_, select
 from sqlalchemy.orm import Session
 
-from app.models.pl_models import PlPlan, PlPlanContract
-from app.schemas.pl.plan import PlanContractGapRead, PlanContractLink
+from app.models.pl_models import PlPlan, PlPlanContract, PlPlanProduct
+from app.schemas.pl.plan import PlanContractGapRead, PlanContractLink, PlanProductRead
 from app.services.pl_data_scope import apply_pl_scope
 from app.services.pl_internal_customer import INTERNAL_CUSTOMER_NAMES, is_internal_customer_name
 from app.services.pl_plan_service import (
@@ -90,6 +90,7 @@ def build_plan_contract_gap_read(
     *,
     supplier_contracts: list[PlanContractLink],
     demander_contracts: list[PlanContractLink],
+    products: Optional[list[PlanProductRead]] = None,
 ) -> PlanContractGapRead:
     gap_sides = compute_contract_gap_sides(
         supplier_name=plan.supplier_name,
@@ -101,6 +102,7 @@ def build_plan_contract_gap_read(
         id=plan.id,
         planNo=plan.plan_no,
         planDate=plan.plan_date,
+        planType=plan.plan_type,
         followStatus=plan.follow_status,
         supplierId=plan.supplier_id,
         supplierName=plan.supplier_name,
@@ -111,9 +113,31 @@ def build_plan_contract_gap_read(
         demanderContractExempt=side_is_exempt(plan.customer_name),
         supplierContracts=supplier_contracts,
         demanderContracts=demander_contracts,
+        products=list(products or []),
     )
 
 
+def _load_products_by_plan_ids(
+    db: Session,
+    plan_ids: list[str],
+) -> dict[str, list[PlanProductRead]]:
+    if not plan_ids:
+        return {}
+    rows = (
+        db.query(PlPlanProduct)
+        .filter(
+            PlPlanProduct.plan_id.in_(plan_ids),
+            PlPlanProduct.del_flag == 0,
+        )
+        .order_by(PlPlanProduct.order_num.asc(), PlPlanProduct.create_time.asc())
+        .all()
+    )
+    out: dict[str, list[PlanProductRead]] = {pid: [] for pid in plan_ids}
+    for row in rows:
+        out.setdefault(row.plan_id, []).append(PlanProductRead.model_validate(row))
+    return out
+
+
 def list_plan_contract_gaps(
     db: Session,
     *,
@@ -185,6 +209,7 @@ def list_plan_contract_gaps(
 
     plan_ids = [plan.id for plan in plans]
     contracts_by_plan = load_plan_contracts_grouped(db, tenant_no, plan_ids)
+    products_by_plan = _load_products_by_plan_ids(db, plan_ids)
 
     items: list[PlanContractGapRead] = []
     for plan in plans:
@@ -194,6 +219,7 @@ def list_plan_contract_gaps(
                 plan,
                 supplier_contracts=supplier_contracts,
                 demander_contracts=demander_contracts,
+                products=products_by_plan.get(plan.id, []),
             )
         )
     return total, items
@@ -254,8 +280,10 @@ def update_plan_side_contracts(
     supplier_contracts, demander_contracts = load_plan_contracts_grouped(
         db, tenant_no, [plan.id]
     ).get(plan.id, ([], []))
+    products = _load_products_by_plan_ids(db, [plan.id]).get(plan.id, [])
     return build_plan_contract_gap_read(
         plan,
         supplier_contracts=supplier_contracts,
         demander_contracts=demander_contracts,
+        products=products,
     )

+ 2 - 0
web-admin/src/api/pl/index.ts

@@ -658,6 +658,7 @@ export interface PlanContractGapItem {
   id: string
   planNo: string
   planDate?: string
+  planType?: string
   followStatus: string
   supplierId?: string
   supplierName?: string
@@ -668,6 +669,7 @@ export interface PlanContractGapItem {
   demanderContractExempt: boolean
   supplierContracts: PlanContractLink[]
   demanderContracts: PlanContractLink[]
+  products?: PlanProduct[]
 }
 
 export interface PlanContractGapQuery {

+ 55 - 0
web-admin/src/views/pl/plan-contract/index.vue

@@ -195,11 +195,18 @@
             </template>
           </el-table-column>
           <el-table-column prop="planDate" label="计划日期" min-width="120" align="center" />
+          <el-table-column prop="planType" label="单据类型" min-width="100" align="center">
+            <template #default="{ row }">
+              {{ getPlanTypeLabel(row.planType) }}
+            </template>
+          </el-table-column>
           <el-table-column
             prop="supplierName"
             label="供方"
             min-width="200"
             align="left"
+            class-name="col-name-wrap"
+            label-class-name="col-name-wrap"
             show-overflow-tooltip
           >
             <template #default="{ row }">
@@ -214,6 +221,8 @@
             label="需方"
             min-width="200"
             align="left"
+            class-name="col-name-wrap"
+            label-class-name="col-name-wrap"
             show-overflow-tooltip
           >
             <template #default="{ row }">
@@ -223,6 +232,42 @@
               </div>
             </template>
           </el-table-column>
+          <el-table-column
+            label="产品名称"
+            min-width="240"
+            align="left"
+            class-name="col-name-wrap"
+            label-class-name="col-name-wrap"
+          >
+            <template #default="{ row }">
+              <PlPlanListProductCell :products="row.products" field="name" />
+            </template>
+          </el-table-column>
+          <el-table-column label="型号/规格" min-width="120" align="center" show-overflow-tooltip>
+            <template #default="{ row }">
+              <PlPlanListProductCell :products="row.products" field="model" />
+            </template>
+          </el-table-column>
+          <el-table-column label="包装规格" min-width="120" align="center" show-overflow-tooltip>
+            <template #default="{ row }">
+              <PlPlanListProductCell :products="row.products" field="packageSpec" />
+            </template>
+          </el-table-column>
+          <el-table-column label="单位" min-width="80" align="center">
+            <template #default="{ row }">
+              <PlPlanListProductCell :products="row.products" field="unit" />
+            </template>
+          </el-table-column>
+          <el-table-column label="计划数量" min-width="100" align="right" header-align="right">
+            <template #default="{ row }">
+              <PlPlanListProductCell :products="row.products" field="planQty" />
+            </template>
+          </el-table-column>
+          <el-table-column label="实际数量" min-width="100" align="right" header-align="right">
+            <template #default="{ row }">
+              <PlPlanListProductCell :products="row.products" field="actualQty" />
+            </template>
+          </el-table-column>
           <el-table-column label="合同缺口" min-width="130" align="center">
             <template #default="{ row }">
               <el-tag :type="gapTag(row).type" size="small" effect="dark">
@@ -289,6 +334,7 @@ import {
   type PlanContractLink
 } from '@/api/pl'
 import PlPlanContractMultiSelect from '@/components/business/pl-plan-contract-multi-select/index.vue'
+import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
 import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
 import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
 import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
@@ -363,6 +409,15 @@ function gapTag(row: PlanContractGapItem) {
   return { text: '—', type: 'info' as const }
 }
 
+function getPlanTypeLabel(type?: string | null) {
+  const map: Record<string, string> = {
+    direct: '直销单',
+    inbound: '入库单',
+    outbound: '出库单'
+  }
+  return type ? map[type] || type : '—'
+}
+
 function partySubText(row: PlanContractGapItem, side: 'supplier' | 'demander') {
   if (side === 'supplier') {
     if (row.supplierContractExempt) return '内置 · 合同可免'