Răsfoiți Sursa

fix:5444【销售订单】在创建订单的界面,查询库存台账界面,增加库存解锁申请的操作

mlchen 1 lună în urmă
părinte
comite
6ba2fa73b4

+ 76 - 6
src/BIZComponents/selectStockLedger/selectStockLedgerDialog.vue

@@ -50,11 +50,28 @@
             class="dict-table"
             @cell-click="cellClick"
             :selection.sync="selection"
+            :current.sync="currentRow"
+            highlight-current-row
             :cache-key="cacheKeyUrl"
             @columns-change="handleColumnChange"
+            @done="handleTableDone"
             :page-size="20"
             @header-dragend="handleHeaderDragend"
           >
+            <template v-slot:toolbar>
+              <el-button
+                v-if="dimension == '2'"
+                type="primary"
+                size="small"
+                @click="openUnlock"
+                >库存解锁</el-button
+              >
+              <span
+                v-if="dimension == '2' && unlockSourceLabel"
+                class="unlock-tip"
+                >{{ unlockSourceLabel }}</span
+              >
+            </template>
             <template v-slot:saleCount="{ row }">
               <el-input
                 v-model="row.saleCount"
@@ -75,6 +92,8 @@
       </ele-split-layout>
     </el-card>
 
+    <unlock-modal ref="unlockModalRef"></unlock-modal>
+
     <div slot="footer">
       <el-button v-if="!isView" type="primary" size="small" @click="selected"
         >选择</el-button
@@ -92,12 +111,13 @@
     getCategoryPackageDisposition
   } from '@/api/wms';
   import ItemSearch from './item-search.vue';
+  import UnlockModal from './unlockModal.vue';
   import { contactQueryByCategoryIdsAPI } from '@/api/saleManage/contact';
   import tabMixins from '@/mixins/tableColumnsMixin';
 
   export default {
     mixins: [tabMixins],
-    components: { ItemSearch },
+    components: { ItemSearch, UnlockModal },
     props: {
       /*已选择的产品*/
       data: {
@@ -137,6 +157,8 @@
         dictList: {},
         curNodeData: {},
         selection: [],
+        // 表格当前高亮行(点击行即选中),用于库存解锁定位库存数据
+        currentRow: null,
         treeData: [],
         dimension: '2',
         defaultProps: {
@@ -182,7 +204,9 @@
             type: 'selection',
             columnKey: 'selection',
             align: 'center',
-            reserveSelection: true,
+            // 不保留勾选:搜索、切换分页/排序后必须清空历史勾选的库存,
+            // reserveSelection 会让 el-table 在数据变化后按 rowKey 恢复勾选,这里必须关闭
+            reserveSelection: false,
             selectable: (row) => {
               return this.getAvailableStockCount(row) != 0;
             }
@@ -354,6 +378,23 @@
         return this.applyColumnWidth(tempData.filter((item) => !item.hide)).map(
           applyWidth
         );
+      },
+      // 库存解锁操作的来源行:优先取唯一勾选行,否则取当前点击高亮行
+      unlockSourceRow() {
+        if (this.selection.length === 1) {
+          return this.selection[0];
+        }
+        return this.currentRow || null;
+      },
+      // 解锁按钮旁的选中数据提示,避免表格级操作定位不明
+      unlockSourceLabel() {
+        const row = this.unlockSourceRow;
+        if (!row) {
+          return '';
+        }
+        const code = row.categoryCode || '-';
+        const batch = row.systemBatchNo || row.batchNo || '-';
+        return `已选:${code} / ${batch} / 锁库 ${row.lockQuantity || 0}`;
       }
     },
     methods: {
@@ -426,11 +467,29 @@
         return data;
       },
 
+      /* 清空表格勾选与当前行:搜索、切换分页/排序、关闭弹窗时都必须重置 */
+      clearTableSelection() {
+        this.selection = [];
+        this.currentRow = null;
+        this.current = null;
+        this.radio = '';
+        if (this.$refs.table && this.$refs.table.clearSelection) {
+          this.$refs.table.clearSelection();
+        }
+      },
+
       /* 刷新表格 */
       reload(where) {
+        // 查询条件变化后原勾选行已不在结果集中,先清空避免带着旧数据继续操作
+        this.clearTableSelection();
         this.$refs.table.reload({ pageNum: 1, where: where });
       },
 
+      /* 表格数据加载完成:分页、排序等表格内部触发的加载同样清空历史勾选 */
+      handleTableDone() {
+        this.clearTableSelection();
+      },
+
       handleNodeClick(data, node) {
         this.curNodeData = data;
         if (data.id == this.currentId) {
@@ -463,6 +522,13 @@
         this.current = row;
         this.radio = row.id;
       },
+      // 打开库存解锁申请弹窗(校验与数据规格化由子弹窗负责)
+      openUnlock() {
+        if (!this.unlockSourceRow) {
+          return this.$message.warning('请先选择一条库存数据进行解锁');
+        }
+        this.$refs.unlockModalRef.open(this.unlockSourceRow);
+      },
       // 列头拖拽结束 - 保存列宽到本地缓存
       handleHeaderDragend(newWidth, oldWidth, column) {
         if (!column.property) return;
@@ -494,11 +560,8 @@
         }
       },
       handleClose() {
+        this.clearTableSelection();
         this.visible = false;
-        this.current = null;
-        this.selection = [];
-        this.$refs.table.clearSelection();
-        this.radio = '';
       },
 
       isSameStockRecord(source, target) {
@@ -634,4 +697,11 @@
   .topsearch {
     margin-bottom: 15px;
   }
+
+  /* 库存解锁按钮旁展示当前选中的库存数据,避免表格级操作定位不明 */
+  .unlock-tip {
+    margin-left: 8px;
+    font-size: 12px;
+    color: #909399;
+  }
 </style>

+ 349 - 0
src/BIZComponents/selectStockLedger/unlockModal.vue

@@ -0,0 +1,349 @@
+<template>
+  <ele-modal
+    title="库存解锁申请"
+    :visible.sync="visible"
+    :before-close="handleClose"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    width="75%"
+    :maxable="true"
+    destroy-on-close
+    @closed="handleClosed"
+    @opened="handleOpened"
+  >
+    <el-card shadow="never">
+      <el-descriptions :column="3" border size="small">
+        <el-descriptions-item label="创建时间">{{
+          formData.createTime || '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="物品编码">{{
+          formData.productCode || '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="名称">{{
+          formData.productName || '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="批次号">{{
+          formData.batchNo || '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="系统内部批次号">{{
+          formData.systemBatchNo || '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="牌号">{{
+          formData.brandNum || '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="型号">{{
+          formData.categoryModel || '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="规格">{{
+          formData.specification || '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="机型">{{
+          formData.modelKey || '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="颜色">{{
+          formData.colorKey || '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="计量单位">{{
+          formData.measureUnit || '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="锁定数量">{{
+          formData.lockQuantity != null ? formData.lockQuantity : '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="仓库">{{
+          formData.warehouseName || '-'
+        }}</el-descriptions-item>
+      </el-descriptions>
+      <div style="margin-top: 12px">
+        <ele-pro-table
+          ref="eleTable"
+          :columns="columns"
+          :datasource="tableData"
+          height="calc(100vh - 405px)"
+          full-height="calc(100vh - 116px)"
+          tool-class="ele-toolbar-form"
+          :pageSize="20"
+          :initLoad="false"
+          :needPage="false"
+          @selection-change="handleSelectionChange"
+        >
+          <template v-slot:unionQuantity="{ row }">
+            <el-input
+              v-model="row.unionQuantity"
+              placeholder="请输入"
+              @input="(value) => inputNum(value, row)"
+            ></el-input>
+          </template>
+        </ele-pro-table>
+      </div>
+    </el-card>
+    <div slot="footer">
+      <el-button
+        :loading="loading"
+        type="primary"
+        size="small"
+        @click="handleSave(0)"
+        >保存</el-button
+      >
+      <el-button
+        :loading="loading"
+        type="primary"
+        size="small"
+        @click="handleSave(1)"
+        >提交</el-button
+      >
+      <el-button size="small" :disabled="loading" @click="handleClose"
+        >取消</el-button
+      >
+    </div>
+  </ele-modal>
+</template>
+<script>
+  import { inboundLockPage, saveUnlock, submitUnlock } from '@/api/wms';
+
+  export default {
+    data() {
+      return {
+        visible: false,
+        loading: false,
+        // 主表数据:展示字段 + 提交时使用白名单字段
+        formData: {},
+        // 该库存维度下的有效锁定明细
+        tableData: [],
+        // 已勾选的解锁明细
+        selectionList: [],
+        columns: [
+          {
+            columnKey: 'selection',
+            type: 'selection',
+            width: 45,
+            align: 'center'
+          },
+          {
+            columnKey: 'index',
+            type: 'index',
+            width: 50,
+            align: 'center',
+            label: '序号',
+            fixed: 'left'
+          },
+          {
+            prop: 'quantity',
+            label: '锁定数量',
+            showOverflowTooltip: true,
+            width: 130,
+            align: 'center'
+          },
+          {
+            prop: 'unionQuantity',
+            label: '解锁数量',
+            slot: 'unionQuantity',
+            showOverflowTooltip: true,
+            width: 130,
+            align: 'center'
+          },
+          {
+            prop: 'sourceOrderNo',
+            label: '关联单据编码',
+            showOverflowTooltip: true,
+            align: 'center'
+          },
+          {
+            prop: 'sourceType',
+            label: '关联单据类型',
+            align: 'center',
+            formatter: (row) =>
+              this.LockOrderSourceTypeEnum[row.sourceType] || '-'
+          },
+          {
+            prop: 'orderDate',
+            label: '锁定时间',
+            align: 'center'
+          }
+        ],
+        LockOrderSourceTypeEnum: {
+          0: '无定义',
+          1: '销售发货单',
+          2: '领料出库单',
+          3: '调拨出库单',
+          4: '销售订单'
+        }
+      };
+    },
+    methods: {
+      /**
+       * 打开解锁弹窗:row 为库存台账行(批次维度)
+       */
+      async open(row) {
+        const productId = row?.categoryId || row?.productId;
+        const productCode = row?.categoryCode || row?.productCode;
+        if (!row?.systemBatchNo && !row?.batchNo) {
+          return this.$message.warning('缺少批次信息,无法发起库存解锁申请');
+        }
+        if (!productId || !productCode || !row?.warehouseId) {
+          return this.$message.warning(
+            '缺少物品或仓库信息,无法发起库存解锁申请'
+          );
+        }
+        if (!(Number(row.lockQuantity) > 0)) {
+          return this.$message.warning('该库存批次没有锁库数量,无需解锁');
+        }
+        this.loading = true;
+        try {
+          // 系统内部批次号为空时,后端要求同时携带批次号、物品ID、物品编码和仓库ID
+          const res = await inboundLockPage({
+            pageNum: 1,
+            size: 500,
+            systemBatchNo: row.systemBatchNo || '',
+            batchNo: row.batchNo || '',
+            productId,
+            productCode,
+            warehouseId: row.warehouseId
+          });
+          const list = res?.list || [];
+          if (!list.length) {
+            return this.$message.warning(
+              '未查询到可解锁的锁库明细,请刷新库存台账后重试'
+            );
+          }
+          // 仅保留后端需要的字段,避免把台账行主键等无关数据回传
+          this.formData = {
+            systemBatchNo: row.systemBatchNo || '',
+            batchNo: row.batchNo || '',
+            productId,
+            productCode,
+            productName: row.categoryName || row.productName || '',
+            warehouseId: row.warehouseId,
+            warehouseName: row.warehouseName || '',
+            createTime: row.createTime,
+            brandNum: row.brandNum,
+            categoryModel: row.categoryModel,
+            specification: row.specification,
+            modelKey: row.modelKey,
+            colorKey: row.colorKey,
+            measureUnit: row.measureUnit,
+            lockQuantity: row.lockQuantity
+          };
+          this.selectionList = [];
+          // 先显示弹窗,再灌入锁定明细:
+          // el-table 在 display:none 的祖先容器内渲染时列宽会算成 0,会出现“有数据但表格看不到行”,
+          // 必须等弹窗显示后再赋值,并在渲染后主动重新布局一次。
+          this.visible = true;
+          this.$nextTick(() => {
+            this.tableData = list.map((item) => ({
+              ...item,
+              unionQuantity: ''
+            }));
+            this.$nextTick(() => {
+              const table = this.$refs.eleTable;
+              if (table && table.doLayout) {
+                table.doLayout();
+              }
+            });
+          });
+        } catch (e) {
+          // 请求异常拦截器已提示 500/-1,这里兜底业务异常并避免未处理的 Promise 拒绝
+          this.$message.error(e?.message || '查询锁库明细失败');
+        } finally {
+          this.loading = false;
+        }
+      },
+      // 解锁数量仅允许数字,且不能超过锁定数量
+      inputNum(value, row) {
+        let numStr = String(value ?? '').replace(/[^\d.]/g, '');
+        // 仅保留一个小数点,避免出现 1.2.3 这类后端无法解析的非法数字
+        const firstDot = numStr.indexOf('.');
+        if (firstDot > -1) {
+          numStr =
+            numStr.slice(0, firstDot + 1) +
+            numStr.slice(firstDot + 1).replace(/\./g, '');
+        }
+        const num = parseFloat(numStr);
+        const max = Number(row.quantity) || 0;
+        if (!isNaN(num) && num > max) {
+          this.$message.warning(`解锁数量不能超过锁定数量(${max})`);
+          this.$set(row, 'unionQuantity', max);
+          return;
+        }
+        this.$set(row, 'unionQuantity', numStr);
+      },
+      handleSelectionChange(selection) {
+        this.selectionList = selection;
+      },
+      handleClose() {
+        this.visible = false;
+      },
+      handleClosed() {
+        this.formData = {};
+        this.tableData = [];
+        this.selectionList = [];
+        this.loading = false;
+      },
+      // 弹窗动画结束后兜底重算列宽,避免表格在隐藏容器内完成布局导致有数据却不显示
+      handleOpened() {
+        const table = this.$refs.eleTable;
+        if (table && table.doLayout) {
+          table.doLayout();
+        }
+      },
+      async handleSave(status) {
+        if (this.loading) {
+          return;
+        }
+        if (!this.selectionList.length) {
+          return this.$message.warning('请选择解锁数据');
+        }
+        const details = [];
+        for (const item of this.selectionList) {
+          const unionQuantity = Number(item.unionQuantity);
+          if (!unionQuantity || unionQuantity <= 0) {
+            return this.$message.error('请填写解锁数量');
+          }
+          if (unionQuantity > Number(item.quantity || 0)) {
+            return this.$message.error('解锁数量不能超过锁定数量');
+          }
+          details.push({
+            lockOrderId: item.lockOrderId,
+            lockOrderDetailId: item.lockOrderDetailId,
+            unionQuantity
+          });
+        }
+        const postData = {
+          systemBatchNo: this.formData.systemBatchNo,
+          batchNo: this.formData.batchNo,
+          productId: this.formData.productId,
+          productCode: this.formData.productCode,
+          productName: this.formData.productName,
+          warehouseId: this.formData.warehouseId,
+          warehouseName: this.formData.warehouseName,
+          details
+        };
+        this.loading = true;
+        let savedId = null;
+        try {
+          savedId = await saveUnlock(postData);
+          if (status === 1) {
+            await submitUnlock({ businessId: savedId });
+            this.$message.success('提交成功');
+          } else {
+            this.$message.success('保存成功');
+          }
+          this.visible = false;
+          this.$emit('success');
+        } catch (e) {
+          if (savedId) {
+            // 草稿已落库,重复保存会产生多条申请,改为提示用户到解锁申请列表重新提交
+            this.visible = false;
+            this.$message.error(
+              `解锁申请已保存为草稿,提交失败:${e?.message || '请稍后重试'}`
+            );
+          } else {
+            this.$message.error(e?.message || '保存失败');
+          }
+        } finally {
+          this.loading = false;
+        }
+      }
+    }
+  };
+</script>

+ 29 - 0
src/api/wms/index.js

@@ -338,3 +338,32 @@ export async function getCategoryPackageDisposition (data){
     }
     return Promise.reject(new Error(res.data.message));
   }
+
+  // 库存台账-按库存维度查询可解锁的锁定明细(系统内部批次号优先,缺失时需带批次号+物品ID+物品编码+仓库ID)
+  export async function inboundLockPage(params) {
+    const res = await request.get(`/wms/lockOrder/v1/inbound-lock-page`, {
+      params
+    });
+    if (res.data.code == 0) {
+      return res.data.data;
+    }
+    return Promise.reject(new Error(res.data.message));
+  }
+
+  // 保存库存解锁申请(草稿),返回申请ID
+  export async function saveUnlock(data) {
+    const res = await request.post(`/wms/inventoryUnlockApprove/save`, data);
+    if (res.data.code == 0) {
+      return res.data.data;
+    }
+    return Promise.reject(new Error(res.data.message));
+  }
+
+  // 提交库存解锁申请审批
+  export async function submitUnlock(data) {
+    const res = await request.post(`/bpm/inventoryUnlockApprove/submit`, data);
+    if (res.data.code == 0) {
+      return res.data.data;
+    }
+    return Promise.reject(new Error(res.data.message));
+  }