From bfa9cabc4fe74da05c3d1eb5148929551c4d87c2 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期四, 28 五月 2026 09:13:01 +0800
Subject: [PATCH] 中兴实强new 1.生产订单领料时如果库存不足可以提交采购申请单 2.采购申请单通知点击跳转是根据合同号直接进行查询

---
 src/views/procurementManagement/procurementLedger/index.vue                         |   26 ++-
 src/layout/components/NotificationCenter/index.vue                                  |    7 
 src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue  |   87 ++++++++--
 src/api/procurementManagement/procurementLedger.js                                  |    9 +
 src/views/productionManagement/productionOrder/components/PurchaseRequestDialog.vue |  308 ++++++++++++++++++++++++++++++++++++++
 5 files changed, 410 insertions(+), 27 deletions(-)

diff --git a/src/api/procurementManagement/procurementLedger.js b/src/api/procurementManagement/procurementLedger.js
index 5f9df05..18c58ca 100644
--- a/src/api/procurementManagement/procurementLedger.js
+++ b/src/api/procurementManagement/procurementLedger.js
@@ -80,6 +80,15 @@
     });
 }
 
+// 淇濆瓨閲囪喘鑽夌锛堝簱瀛樹笉瓒冲満鏅級
+export function saveShortagePurchaseDraft(data) {
+    return request({
+        url: "/purchase/ledger/saveShortagePurchaseDraft",
+        method: "post",
+        data: data,
+    });
+}
+
 // 淇濆瓨閲囪喘妯℃澘
 export function addPurchaseTemplate(data) {
     return request({
diff --git a/src/layout/components/NotificationCenter/index.vue b/src/layout/components/NotificationCenter/index.vue
index 66f489a..2d3df05 100644
--- a/src/layout/components/NotificationCenter/index.vue
+++ b/src/layout/components/NotificationCenter/index.vue
@@ -190,6 +190,13 @@
               });
             }
 
+            // 濡傛灉鏄噰璐敵璇锋彁閱掞紝鏍规嵁purchaseContractNumber鏌ヨ
+            if (item.noticeTitle === "閲囪喘鐢宠鎻愰啋" && query.purchaseContractNumber) {
+              query = {
+                purchaseContractNumber: query.purchaseContractNumber,
+              };
+            }
+
             // 璺宠浆鍒版寚瀹氶〉闈�
             router.push({
               path: path,
diff --git a/src/views/procurementManagement/procurementLedger/index.vue b/src/views/procurementManagement/procurementLedger/index.vue
index dad6a26..6a50276 100644
--- a/src/views/procurementManagement/procurementLedger/index.vue
+++ b/src/views/procurementManagement/procurementLedger/index.vue
@@ -699,6 +699,7 @@
     getCurrentInstance,
     nextTick,
   } from "vue";
+  import { useRoute } from "vue-router";
   import { Search, Delete } from "@element-plus/icons-vue";
   import { ElMessageBox, ElMessage } from "element-plus";
   import { userListNoPage } from "@/api/system/user.js";
@@ -728,6 +729,7 @@
   );
 
   const { proxy } = getCurrentInstance();
+  const route = useRoute();
   const { tax_rate } = proxy.useDict("tax_rate");
   const tableData = ref([]);
   const productData = ref([]);
@@ -756,6 +758,7 @@
 
   // 璁㈠崟瀹℃壒鐘舵�佹樉绀烘枃鏈�
   const approvalStatusText = {
+    0: "鑽夌",
     1: "寰呭鏍�",
     2: "瀹℃壒涓�",
     3: "瀹℃壒閫氳繃",
@@ -765,6 +768,7 @@
   // 鑾峰彇瀹℃壒鐘舵�佹爣绛剧被鍨�
   const getApprovalStatusType = status => {
     const typeMap = {
+      0: "info", // 寰呭鏍� - 鐏拌壊
       1: "info", // 寰呭鏍� - 鐏拌壊
       2: "warning", // 瀹℃壒涓� - 姗欒壊
       3: "success", // 瀹℃壒閫氳繃 - 缁胯壊
@@ -936,13 +940,10 @@
   const { form, rules } = toRefs(data);
   const { form: searchForm } = useFormData({
     ...data.searchForm,
-    // 璁剧疆褰曞叆鏃ユ湡鑼冨洿涓哄綋澶�
-    entryDate: [
-      dayjs().startOf("day").format("YYYY-MM-DD"),
-      dayjs().endOf("day").format("YYYY-MM-DD"),
-    ],
-    entryDateStart: dayjs().startOf("day").format("YYYY-MM-DD"),
-    entryDateEnd: dayjs().endOf("day").format("YYYY-MM-DD"),
+    // 褰曞叆鏃ユ湡涓嶈缃粯璁ゅ��
+    entryDate: null,
+    entryDateStart: undefined,
+    entryDateEnd: undefined,
   });
 
   // 浜у搧琛ㄥ崟寮规鏁版嵁
@@ -1919,7 +1920,16 @@
   };
 
   onMounted(() => {
-    getList();
+    // 妫�鏌RL鍙傛暟涓槸鍚︽湁purchaseContractNumber锛屾湁鍒欒缃埌鎼滅储鏉′欢
+    if (route.query.purchaseContractNumber) {
+      // 浣跨敤setTimeout纭繚searchForm宸茬粡鍒濆鍖�
+      setTimeout(() => {
+        searchForm.purchaseContractNumber = route.query.purchaseContractNumber;
+        getList();
+      }, 0);
+    } else {
+      getList();
+    }
     getTemplateList();
   });
 </script>
diff --git a/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue b/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
index 09e7421..85fbe35 100644
--- a/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
+++ b/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
@@ -2,7 +2,7 @@
   <div>
     <el-dialog v-model="dialogVisible"
                title="棰嗘枡鍙拌处"
-               width="1200px"
+               width="1400px"
                @close="handleClose">
       <div class="material-toolbar">
         <el-button type="primary"
@@ -65,10 +65,18 @@
             </el-select>
           </template>
         </el-table-column>
+        <el-table-column label="搴撳瓨鏁伴噺"
+                         min-width="120">
+          <template #default="{ row }">
+            <span :class="{ 'text-danger': isStockInsufficient(row) }">
+              {{ row.stockQuantity ?? '-' }}
+            </span>
+          </template>
+        </el-table-column>
         <el-table-column label="闇�姹傛暟閲�"
                          min-width="120">
           <template #default="{ row }">
-            <span v-if="row.bom === true">{{ row.demandedQuantity ?? "-" }}</span>
+            <span v-if="row.bom === true" :class="{ 'text-danger': isStockInsufficient(row) }">{{ row.demandedQuantity ?? "-" }}</span>
             <el-input-number v-else
                              v-model="row.demandedQuantity"
                              :min="0"
@@ -76,6 +84,7 @@
                              :step="1"
                              controls-position="right"
                              style="width: 100%;"
+                             :class="{ 'is-stock-insufficient': isStockInsufficient(row) }"
                              @change="val => handleRequiredQtyChange(row, val)" />
           </template>
         </el-table-column>
@@ -109,6 +118,9 @@
       </el-table>
       <template #footer>
         <span class="dialog-footer">
+          <el-button v-if="hasInsufficientStock"
+                     type="warning"
+                     @click="openPurchaseRequestDialog">閲囪喘鐢宠</el-button>
           <el-button type="primary"
                      :loading="materialSaving"
                      :disabled="isSaveDisabled"
@@ -120,6 +132,10 @@
     <ProductSelectDialog v-model="materialProductDialogVisible"
                          @confirm="handleMaterialProductConfirm"
                          single />
+    <PurchaseRequestDialog v-model="purchaseRequestDialogVisible"
+                           :insufficient-items="insufficientStockItems"
+                           :order-row="props.orderRow"
+                           @saved="handlePurchaseRequestSaved" />
     <!-- request-url="/stockInventory/rawMaterials" -->
   </div>
 </template>
@@ -128,18 +144,18 @@
   import { computed, ref, watch } from "vue";
   import { ElMessage } from "element-plus";
   import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+  import PurchaseRequestDialog from "./PurchaseRequestDialog.vue";
   import {
     findProductProcessRouteItemList,
     listMain,
   } from "@/api/productionManagement/productProcessRoute.js";
   import {
-    listMaterialPickingDetail,
     listMaterialPickingBom,
-    listMaterialPickingLedger,
     saveMaterialPickingLedger,
     updateMaterialPickingLedger,
   } from "@/api/productionManagement/productionOrder.js";
   import { queryList2 } from "@/api/productionManagement/productStructure.js";
+
 
   const props = defineProps({
     modelValue: { type: Boolean, default: false },
@@ -156,6 +172,7 @@
   const materialTableLoading = ref(false);
   const materialSaving = ref(false);
   const materialTableData = ref([]);
+  const purchaseRequestDialogVisible = ref(false);
 
   const isSaveDisabled = computed(() => {
     if (materialTableData.value.length === 0) return true;
@@ -183,6 +200,23 @@
     });
   });
 
+  // 鍒ゆ柇搴撳瓨鏄惁涓嶈冻
+  const isStockInsufficient = (row) => {
+    const stockQuantity = Number(row.stockQuantity ?? 0);
+    const demandedQty = Number(row.demandedQuantity ?? 0);
+    return demandedQty > 0 && stockQuantity > 0 && demandedQty > stockQuantity;
+  };
+
+  // 搴撳瓨涓嶈冻鐨勮
+  const insufficientStockItems = computed(() => {
+    return materialTableData.value.filter(row => isStockInsufficient(row));
+  });
+
+  // 鏄惁鏈夊簱瀛樹笉瓒�
+  const hasInsufficientStock = computed(() => {
+    return insufficientStockItems.value.length > 0;
+  });
+
   const processOptions = ref([]);
   const currentMaterialSelectRowIndex = ref(-1);
   let materialTempId = 0;
@@ -206,6 +240,7 @@
         : row.batchNo
       : [],
     batchNoList: row.batchNoList || [],
+    stockQuantity: row.stockQuantity ?? row.stockQty ?? null,
   });
 
   const getProcessOptions = async () => {
@@ -239,22 +274,14 @@
     materialTableData.value = [];
     await getProcessOptions();
     try {
-      const detailRes = await listMaterialPickingDetail(props.orderRow.id);
-      const detailList = Array.isArray(detailRes?.data)
-        ? detailRes.data
-        : detailRes?.data?.records || [];
-      if (detailList.length > 0) {
-        isDetail.value = true;
-        materialTableData.value = detailList.map(item => createMaterialRow(item));
-        return;
-      } else {
+      // 鐩存帴璋冪敤listMaterialPickingBom鎺ュ彛鑾峰彇搴撳瓨鏁伴噺
+      const bomRes = await listMaterialPickingBom(props.orderRow.id);
+      const bomList = Array.isArray(bomRes?.data)
+        ? bomRes.data
+        : bomRes?.data?.records || [];
+      if (bomList.length > 0) {
         isDetail.value = false;
-        const bomRes = await listMaterialPickingBom(props.orderRow.id);
-        const bomList = Array.isArray(bomRes?.data)
-          ? bomRes.data
-          : bomRes?.data?.records || [];
         materialTableData.value = bomList.map(item => createMaterialRow(item));
-        return;
       }
     } finally {
       materialTableLoading.value = false;
@@ -305,7 +332,7 @@
     materialProductDialogVisible.value = true;
   };
 
-  const handleMaterialProductConfirm = products => {
+  const handleMaterialProductConfirm = async (products) => {
     console.log(products, "products");
 
     if (!products || products.length === 0) return;
@@ -417,6 +444,17 @@
       materialSaving.value = false;
     }
   };
+
+  // 鎵撳紑閲囪喘鐢宠瀵硅瘽妗�
+  const openPurchaseRequestDialog = () => {
+    purchaseRequestDialogVisible.value = true;
+  };
+
+  // 閲囪喘鐢宠淇濆瓨鍥炶皟
+  const handlePurchaseRequestSaved = () => {
+    // 閲囪喘鐢宠淇濆瓨鎴愬姛鍚庡埛鏂版暟鎹�
+    loadMaterialData();
+  };
 </script>
 
 <style scoped lang="scss">
@@ -424,4 +462,15 @@
     margin-bottom: 12px;
     text-align: right;
   }
+
+  .text-danger {
+    color: #f56c6c;
+    font-weight: bold;
+  }
+
+  :deep(.is-stock-insufficient) {
+    .el-input__wrapper {
+      box-shadow: 0 0 0 1px #f56c6c inset;
+    }
+  }
 </style>
diff --git a/src/views/productionManagement/productionOrder/components/PurchaseRequestDialog.vue b/src/views/productionManagement/productionOrder/components/PurchaseRequestDialog.vue
new file mode 100644
index 0000000..0000c26
--- /dev/null
+++ b/src/views/productionManagement/productionOrder/components/PurchaseRequestDialog.vue
@@ -0,0 +1,308 @@
+<template>
+  <div>
+    <el-dialog v-model="dialogVisible"
+               title="閲囪喘鐢宠锛堝簱瀛樹笉瓒筹級"
+               width="900px"
+               @close="handleClose">
+      <!-- 绠�鏄撻噰璐敵璇疯〃鍗� -->
+      <el-form :model="form" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="閿�鍞鍗曞彿" required>
+              <el-input v-model="form.salesContractNo" placeholder="璇疯緭鍏ラ攢鍞鍗曞彿" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="鎶勯�佷汉" required>
+              <el-select v-model="form.ccUserId" placeholder="璇烽�夋嫨鎶勯�佷汉" style="width: 100%" filterable>
+                <el-option v-for="user in userOptions" :key="user.userId" :label="user.nickName" :value="user.userId" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+
+      <!-- 浜у搧琛ㄦ牸 -->
+      <div class="purchase-request-table">
+        <div class="table-toolbar">
+          <span class="table-title">閲囪喘浜у搧鏄庣粏</span>
+          <el-button type="primary" @click="handleAddRow">鏂板浜у搧</el-button>
+        </div>
+        <el-table :data="tableData" border style="width: 100%;" max-height="400">
+          <el-table-column type="index" label="搴忓彿" width="60" align="center" />
+          <el-table-column label="浜у搧鍚嶇О" min-width="150">
+            <template #default="{ row }">
+              <el-button v-if="!row.productName" type="primary" link @click="openProductSelect(row)">
+                閫夋嫨浜у搧
+              </el-button>
+              <span v-else>{{ row.productName }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="鍨嬪彿/瑙勬牸" min-width="150">
+            <template #default="{ row }">
+              {{ row.model || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="鍗曚綅" width="80" align="center">
+            <template #default="{ row }">
+              {{ row.unit || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="鏁伴噺" width="120">
+            <template #default="{ row }">
+              <el-input-number v-model="row.quantity" :min="1" :precision="0" :step="1" controls-position="right" style="width: 100%;" />
+            </template>
+          </el-table-column>
+          <el-table-column label="鎿嶄綔" width="80" align="center" fixed="right">
+            <template #default="{ $index }">
+              <el-button type="danger" link @click="handleDeleteRow($index)">鍒犻櫎</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button type="primary" :loading="saving" @click="handleSave">淇濆瓨鑽夌</el-button>
+          <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <ProductSelectDialog v-model="productSelectVisible" @confirm="handleProductConfirm" single />
+  </div>
+</template>
+
+<script setup>
+import { computed, ref, watch, onMounted } from "vue";
+import { ElMessage } from "element-plus";
+import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+import { saveShortagePurchaseDraft } from "@/api/procurementManagement/procurementLedger.js";
+import { listUser } from "@/api/system/user.js";
+import useUserStore from "@/store/modules/user.js";
+
+const userStore = useUserStore();
+
+const props = defineProps({
+  modelValue: { type: Boolean, default: false },
+  insufficientItems: { type: Array, default: () => [] },
+  orderRow: { type: Object, default: null },
+});
+const emit = defineEmits(["update:modelValue", "saved"]);
+
+const dialogVisible = computed({
+  get: () => props.modelValue,
+  set: (val) => emit("update:modelValue", val),
+});
+
+const productSelectVisible = ref(false);
+const saving = ref(false);
+const currentSelectRowIndex = ref(-1);
+const userOptions = ref([]);
+
+// 琛ㄥ崟鏁版嵁
+const form = ref({
+  salesContractNo: "",
+  ccUserId: null,
+  ccUserName: "",
+});
+
+// 琛ㄦ牸鏁版嵁
+const tableData = ref([]);
+
+// 鑾峰彇鐢ㄦ埛鍒楄〃锛堟妱閫佷汉閫夋嫨锛�
+const getUserList = async () => {
+  try {
+    const res = await listUser({ pageSize: 1000 });
+    userOptions.value = res.rows || [];
+  } catch (error) {
+    console.error("鑾峰彇鐢ㄦ埛鍒楄〃澶辫触:", error);
+  }
+};
+
+onMounted(() => {
+  getUserList();
+});
+
+// 鐩戝惉瀵硅瘽妗嗘墦寮�锛屽垵濮嬪寲鏁版嵁
+watch(
+  () => dialogVisible.value,
+  (visible) => {
+    if (visible) {
+      initData();
+    }
+  }
+);
+
+// 鍒濆鍖栨暟鎹�
+const initData = () => {
+  // 浠庣敓浜ц鍗曚腑鑾峰彇閿�鍞鍗曞彿
+  form.value.salesContractNo = props.orderRow?.salesContractNo || "";
+  form.value.applicantId = userStore.id || "";
+  form.value.applicantName = userStore.name || "";
+  form.value.ccUserId = null;
+  form.value.ccUserName = "";
+
+  // 灏嗗簱瀛樹笉瓒崇殑浜у搧濉厖鍒拌〃鏍�
+  tableData.value = props.insufficientItems.map((item) => ({
+    tempId: generateTempId(),
+    productModelId: item.materialModelId,
+    productName: item.materialName,
+    model: item.materialModel,
+    unit: item.unit,
+    quantity: Math.max(1, Math.ceil((item.demandedQuantity || 0) - (item.stockQuantity || 0))),
+  }));
+};
+
+// 鐢熸垚涓存椂ID
+let tempIdCounter = 0;
+const generateTempId = () => {
+  return `temp_${++tempIdCounter}_${Date.now()}`;
+};
+
+// 鍏抽棴瀵硅瘽妗�
+const handleClose = () => {
+  form.value = {
+    salesContractNo: "",
+    applicantId: "",
+    applicantName: "",
+    ccUserId: null,
+    ccUserName: "",
+  };
+  tableData.value = [];
+  currentSelectRowIndex.value = -1;
+};
+
+// 鏂板琛�
+const handleAddRow = () => {
+  tableData.value.push({
+    tempId: generateTempId(),
+    productModelId: null,
+    productName: "",
+    model: "",
+    unit: "",
+    quantity: 1,
+  });
+};
+
+// 鍒犻櫎琛�
+const handleDeleteRow = (index) => {
+  tableData.value.splice(index, 1);
+};
+
+// 鎵撳紑浜у搧閫夋嫨
+const openProductSelect = (row) => {
+  currentSelectRowIndex.value = tableData.value.findIndex(
+    (item) => item.tempId === row.tempId
+  );
+  productSelectVisible.value = true;
+};
+
+// 浜у搧閫夋嫨纭
+const handleProductConfirm = (products) => {
+  if (!products || products.length === 0) return;
+  const index = currentSelectRowIndex.value;
+  if (index < 0 || !tableData.value[index]) return;
+  const product = products[0];
+  const row = tableData.value[index];
+  row.productModelId = product.materialModelId || product.modelId || product.id;
+  row.productName = product.materialName || product.productName || product.name || "";
+  row.model = product.materialModel || product.model || "";
+  row.unit = product.unit || product.measureUnit || "";
+  currentSelectRowIndex.value = -1;
+  productSelectVisible.value = false;
+};
+
+// 楠岃瘉琛ㄥ崟
+const validateForm = () => {
+  if (!form.value.salesContractNo) {
+    ElMessage.warning("璇疯緭鍏ラ攢鍞鍗曞彿");
+    return false;
+  }
+  if (!form.value.ccUserId) {
+    ElMessage.warning("璇烽�夋嫨鎶勯�佷汉");
+    return false;
+  }
+  if (tableData.value.length === 0) {
+    ElMessage.warning("璇疯嚦灏戞坊鍔犱竴涓骇鍝�");
+    return false;
+  }
+  for (let i = 0; i < tableData.value.length; i++) {
+    const row = tableData.value[i];
+    if (!row.productName) {
+      ElMessage.warning(`绗�${i + 1}琛岃閫夋嫨浜у搧`);
+      return false;
+    }
+    if (!row.quantity || row.quantity <= 0) {
+      ElMessage.warning(`绗�${i + 1}琛屾暟閲忓繀椤诲ぇ浜�0`);
+      return false;
+    }
+  }
+  return true;
+};
+
+// 淇濆瓨鑽夌
+const handleSave = async () => {
+  if (!validateForm()) return;
+
+  // 鑾峰彇鎶勯�佷汉濮撳悕
+  const selectedUser = userOptions.value.find(u => u.userId === form.value.ccUserId);
+  form.value.ccUserName = selectedUser?.userName || "";
+
+  saving.value = true;
+  try {
+    // 鏋勫缓閲囪喘鑽夌鏁版嵁锛堟牴鎹仈璋冩枃妗� PurchaseLedgerDto 鏍煎紡锛�
+    const draftData = {
+      salesContractNo: form.value.salesContractNo,
+      ccUserId: form.value.ccUserId,
+      ccUserName: form.value.ccUserName,
+      // 浜у搧鏄庣粏鏁版嵁
+      productData: tableData.value.map((item) => ({
+        productModelId: item.productModelId,
+        productName: item.productName,
+        model: item.model,
+        unit: item.unit,
+        quantity: item.quantity,
+      })),
+    };
+
+    const res = await saveShortagePurchaseDraft(draftData);
+    if (res.code === 200) {
+      ElMessage.success("閲囪喘鑽夌淇濆瓨鎴愬姛锛屽凡閫氱煡鎶勯�佷汉琛ュ叏淇℃伅");
+      emit("saved", res.data); // 杩斿洖鑽夌ID
+      dialogVisible.value = false;
+    } else {
+      ElMessage.error(res.msg || "淇濆瓨澶辫触");
+    }
+  } catch (error) {
+    console.error("淇濆瓨閲囪喘鑽夌澶辫触:", error);
+    ElMessage.error("淇濆瓨澶辫触锛岃閲嶈瘯");
+  } finally {
+    saving.value = false;
+  }
+};
+</script>
+
+<style scoped lang="scss">
+.purchase-request-table {
+  margin-top: 20px;
+
+  .table-toolbar {
+    margin-bottom: 10px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .table-title {
+      font-weight: bold;
+      font-size: 14px;
+    }
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: center;
+  gap: 10px;
+}
+</style>

--
Gitblit v1.9.3