中兴实强new
1.生产订单领料时如果库存不足可以提交采购申请单
2.采购申请单通知点击跳转是根据合同号直接进行查询
已添加1个文件
已修改4个文件
437 ■■■■■ 文件已修改
src/api/procurementManagement/procurementLedger.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/NotificationCenter/index.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue 87 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/PurchaseRequestDialog.vue 308 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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({
src/layout/components/NotificationCenter/index.vue
@@ -190,6 +190,13 @@
              });
            }
            // å¦‚果是采购申请提醒,根据purchaseContractNumber查询
            if (item.noticeTitle === "采购申请提醒" && query.purchaseContractNumber) {
              query = {
                purchaseContractNumber: query.purchaseContractNumber,
              };
            }
            // è·³è½¬åˆ°æŒ‡å®šé¡µé¢
            router.push({
              path: path,
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();
    // æ£€æŸ¥URL参数中是否有purchaseContractNumber,有则设置到搜索条件
    if (route.query.purchaseContractNumber) {
      // ä½¿ç”¨setTimeout确保searchForm已经初始化
      setTimeout(() => {
        searchForm.purchaseContractNumber = route.query.purchaseContractNumber;
        getList();
      }, 0);
    } else {
      getList();
    }
    getTemplateList();
  });
</script>
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>
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>