pan
2025-11-29 6641d42d35d7b9739c64fe578d69e43a39e26c16
提交
已添加3个文件
已修改14个文件
2150 ■■■■ 文件已修改
项目代码/WIDESEA_WMSClient/src/extension/outbound/extend/outOrderDetail.vue 208 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/router/viewGird.js 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/outbound/BatchPickingConfirm.vue 1066 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/17.14.878.3237/CodeChunks.db-shm 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/17.14.878.3237/SemanticSymbols.db-shm 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Common/OrderEnum/OutboundOrderEnum.cs 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Common/StockEnum/OutLockStockStatusEnum.cs 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_IOutboundService/IOutboundBatchPickingService.cs 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_InboundService/InboundOrderService.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Model/Models/Outbound/Dt_PickingRecord.cs 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundBatchPickingService.cs 609 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundOrderDetailService.cs 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundOrderService.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundPickingService.cs 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_Outbound.cs 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Outbound/OutboundBatchPickingController.cs 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/extension/outbound/extend/outOrderDetail.vue
@@ -27,13 +27,25 @@
                style="float: right; height: 20px"
                @click="handleOpenPicking"
                >拣选</el-link>
                        <el-link
                type="primary"
                size="small"
                style="float: right; height: 20px; margin-right: 10px"
                @click="handleOpenBatchPicking"
                >分批拣选</el-link>
              <el-link
                type="primary"
                size="small"
                style="float: right; height: 20px; margin-right: 10px"
                @click="outbound"
                >直接出库</el-link
              >
               <el-link
                type="primary"
                size="small"
                style="float: right; height: 20px; margin-right: 10px"
                @click="outboundbatch"
                >分批出库</el-link
              >
              <el-link
                type="primary"
@@ -113,8 +125,9 @@
import SelectedStock from "./SelectedStock.vue";
import NoStockOut from "./NoStockOut.vue";
import { h,createVNode, render,reactive  } from 'vue';
import { ElDialog , ElForm, ElFormItem, ElSelect,ElOption, ElButton, ElMessage } from 'element-plus';
import { ElDialog , ElForm, ElFormItem, ElSelect,ElOption, ElButton, ElInput, ElMessage } from 'element-plus';
import { th } from 'element-plus/es/locale';
export default {
  components: { VolBox, VolForm, StockSelect, SelectedStock,NoStockOut},
  data() {
@@ -344,6 +357,11 @@
        query: { orderId: this.row.id ,orderNo:this.row.orderNo}
      })
    },
    handleOpenBatchPicking() {
      this.$router.push({
        path: '/outbound/batchpicking',
        query: { orderId: this.row.id ,orderNo:this.row.orderNo}})
    },
    outbound() {
      if (this.selection.length === 0) {
        return this.$message.error("请选择单据明细");
@@ -481,6 +499,192 @@
      vnode.appContext = this.$.appContext;
      render(vnode, mountNode);
    },
    outboundbatch() {
  if (this.selection.length === 0) {
    return this.$message.error("请选择单据明细");
  }
    if (this.selection.length>1) {
    return this.$message.error("只能选择一条单据明细进行分批出库");
  }
  const platformOptions = [{label:'站台2',value:'2-1'},{label:'站台3',value:'3-1'}];
  const mountNode = document.createElement('div');
  document.body.appendChild(mountNode);
  // 2. è¡¨å•数据(默认选中站台3,新增小数字段)
  const formData = reactive({
    selectedPlatform: platformOptions[0].value, // é»˜è®¤ç»‘定「站台3」的value
    outboundDecimal: '' // æ–°å¢žï¼šå°æ•°è¾“入框字段
  });
  // 3. åŠ¨æ€åˆ›å»ºå¼¹çª—ç»„ä»¶
  const vnode = createVNode(ElDialog, {
    title: '出库操作 - é€‰æ‹©å‡ºåº“站台',
    width: '500px',
    modelValue: true,
    appendToBody: true,
    'onUpdate:modelValue': (isVisible) => {
      if (!isVisible) {
        render(null, mountNode);
        document.body.removeChild(mountNode);
      }
    },
    style: {
      padding: '20px 0',
      borderRadius: '8px'
    }
  }, {
    default: () => h(ElForm, {
      model: formData,
      rules: {
        selectedPlatform: [
          { required: true, message: '请选择出库站台', trigger: 'change' }
        ],
        // æ–°å¢žï¼šå°æ•°å­—段验证规则(必填 + æœ‰æ•ˆå°æ•°æ ¼å¼ï¼‰
        outboundDecimal: [
          { required: true, message: '请输入小数数值', trigger: 'blur' },
          {
            validator: (rule, value, callback) => {
              // éªŒè¯è§„则:正数、支持小数点后最多2位(可根据需求调整小数位数)
              const decimalReg = /^(([1-9]\d*)|0)(\.\d{1,2})?$/;
              if (value && !decimalReg.test(value)) {
                callback(new Error('请输入有效的小数(正数,最多2位小数)'));
              } else {
                callback();
              }
            },
            trigger: 'blur'
          }
        ]
      },
      ref: 'outboundForm',
      labelWidth: '100px',
      style: {
        padding: '0 30px'
      }
    }, [
      // å‡ºåº“站台选择项(核心表单项)
      h(ElFormItem, {
        label: '出库站台',
        prop: 'selectedPlatform',
        style: {
          marginBottom: '24px'
        }
      }, [
        h(ElSelect, {
          placeholder: '请选择出库站台(3-12)',
          modelValue: formData.selectedPlatform,
          'onUpdate:modelValue': (val) => {
            formData.selectedPlatform = val;
          },
          style: {
            width: '100%',
            height: '40px',
            borderRadius: '4px',
            borderColor: '#dcdfe6'
          }
        }, platformOptions.map(platform =>
          h(ElOption, { label: platform.label, value: platform.value })
        ))
      ]),
      // æ–°å¢žï¼šå°æ•°è¾“入框表单项
      h(ElFormItem, {
        label: '出库数', // å¯æ ¹æ®ä¸šåŠ¡éœ€æ±‚ä¿®æ”¹æ ‡ç­¾åï¼ˆå¦‚â€œå‡ºåº“æ•°é‡â€â€œé‡é‡â€ç­‰ï¼‰
        prop: 'outboundDecimal',
        style: {
          marginBottom: '24px'
        }
      }, [
        h(ElInput, {
          type: 'number', // æ•°å­—类型,原生支持小数输入
          placeholder: '请输入小数数值(最多2位小数)',
          modelValue: formData.outboundDecimal,
          'onUpdate:modelValue': (val) => {
            formData.outboundDecimal = val;
          },
          style: {
            width: '100%',
            height: '40px',
            borderRadius: '4px',
            borderColor: '#dcdfe6'
          },
          step: '0.01', // æ­¥é•¿0.01,点击上下箭头时按0.01增减
          precision: 2, // é™åˆ¶æœ€å¤šè¾“å…¥2位小数(Element Plus属性)
          min: 0.01, // å¯é€‰ï¼šé™åˆ¶æœ€å°å€¼ä¸º0.01,避免输入0或负数
        })
      ]),
      // åº•部按钮区
      h('div', {
        style: {
          textAlign: 'right',
          marginTop: '8px',
          paddingRight: '4px'
        }
      }, [
        h(ElButton, {
          type: 'text',
          onClick: () => {
            render(null, mountNode);
            document.body.removeChild(mountNode);
            ElMessage.info('取消分批出库操作');
          },
          style: {
            marginRight: '8px',
            color: '#606266'
          }
        }, '取消'),
        h(ElButton, {
          type: 'primary',
          onClick: async () => {
            const formRef = vnode.component.refs.outboundForm;
            try {
              // è¡¨å•校验(会同时校验出库站台和小数字段)
              await formRef.validate();
            } catch (err) {
              return;
            }
            // 4. æž„造请求参数(新增小数字段)
            const keys = this.selection.map((item) => item.id);
            const requestParams = {
              taskIds: keys,
              outboundPlatform: formData.selectedPlatform, // å‡ºåº“站台
              outboundDecimal: formData.outboundDecimal // æ–°å¢žï¼šå°æ•°å­—段传给后端
            };
            // 5. è°ƒç”¨å‡ºåº“接口
            this.http
              .post("api/Task/ ", requestParams, "数据处理中")
              .then((x) => {
                if (!x.status) return ElMessage.error(x.message);
                ElMessage.success("操作成功");
                this.showDetialBox = false; // å…³é—­è¯¦æƒ…框
                this.$emit("parentCall", ($vue) => {
                  $vue.getData(); // é€šçŸ¥çˆ¶ç»„件刷新
                });
                // å…³é—­å¼¹çª—
                render(null, mountNode);
                document.body.removeChild(mountNode);
              })
              .catch(() => {
                ElMessage.error('请求失败,请稍后重试');
              });
          },
          style: {
            borderRadius: '4px',
            padding: '8px 20px'
          }
        }, '确定分批出库')
      ])
    ])
  });
  // ç»‘定app上下文,确保El组件正常工作
  vnode.appContext = this.$.appContext;
  render(vnode, mountNode);
},
    setCurrent(row) {
      this.$refs.singleTable.setCurrentRow(row);
    },
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/router/viewGird.js
@@ -79,7 +79,14 @@
  name: 'PickingConfirm', 
  component: () => import('@/views/outbound/PickingConfirm.vue'),
  meta: { title: '拣选确认', keepAlive: false }
},{
},
  {
  path: '/outbound/batchpicking',
  name: 'BatchPickingConfirm',
  component: () => import('@/views/outbound/BatchPickingConfirm.vue'),
  meta: { title: '拣选确认', keepAlive: false }
},
{
    path: '/stockInfo',
    name: 'stockInfo',
    component: () => import('@/views/stock/stockInfo.vue')
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/outbound/BatchPickingConfirm.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1066 @@
<template>
  <div class="OutboundPicking-container">
    <div class="page-header">
      <el-page-header @back="goBack">
        <template #content>
          <span class="title">出库拣选确认 - {{ this.$route.query.orderNo }}</span>
          <el-tag v-if="currentBatchNo" type="success" style="margin-left: 10px;">
            å½“前批次: {{ currentBatchNo }}
          </el-tag>
        </template>
      </el-page-header>
    </div>
    <!-- æ‰¹æ¬¡æ“ä½œåŒºåŸŸ -->
    <div class="batch-operations">
      <el-card>
        <div class="batch-actions">
          <el-button type="primary" @click="openBatchAllocateDialog">分批分配</el-button>
          <el-select v-model="selectedBatchNo" placeholder="选择批次" @change="onBatchChange" style="width: 200px; margin-left: 10px;">
            <el-option
              v-for="batch in batchList"
              :key="batch.batchNo"
              :label="`${batch.batchNo} (${batch.batchStatusText})`"
              :value="batch.batchNo">
            </el-option>
          </el-select>
          <el-button type="info" @click="refreshBatchList">刷新批次</el-button>
        </div>
      </el-card>
    </div>
    <!-- æ‰«ç åŒºåŸŸ -->
    <div class="scanner-area">
      <el-card>
        <div class="scanner-form">
          <el-input
            ref="palletInput"
            v-model="scanData.palletCode"
            placeholder="扫描托盘码"
            @change="onPalletScan"
            @keyup.enter.native="onPalletScan">
          </el-input>
          <el-input
            ref="barcodeInput"
            v-model="scanData.barcode"
            placeholder="扫描物料条码"
            @change="onBarcodeScan"
            @keyup.enter.native="onBarcodeScan">
          </el-input>
          <el-button type="success" @click="confirmPicking">确认拣选</el-button>
          <el-button type="warning" @click="openSplitDialog">拆包</el-button>
          <el-button type="info" @click="openRevertSplitDialog">撤销拆包</el-button>
          <el-button type="info" @click="handleEmptyPallet">取空箱</el-button>
          <el-button type="primary" @click="openBatchReturnDialog">回库</el-button>
        </div>
      </el-card>
    </div>
    <!-- æ‰¹æ¬¡æ±‡æ€»ä¿¡æ¯ -->
    <div class="batch-summary-area" v-if="currentBatchNo">
      <el-card>
        <div class="batch-summary-info">
          <el-tag type="info">批次号: {{ batchSummary.batchNo }}</el-tag>
          <el-tag :type="getBatchStatusType(batchSummary.batchStatus)">
            {{ batchSummary.batchStatusText }}
          </el-tag>
          <el-tag type="warning">分配数量: {{ batchSummary.batchQuantity }}</el-tag>
          <el-tag type="success">完成数量: {{ batchSummary.completedQuantity }}</el-tag>
          <el-tag type="danger">剩余数量: {{ batchSummary.remainingQuantity }}</el-tag>
        </div>
      </el-card>
    </div>
    <!-- æ±‡æ€»ä¿¡æ¯ -->
    <div class="summary-area">
      <el-card>
        <div class="summary-info">
          <el-tag type="warning">未拣选条数: {{summary.unpickedCount}}</el-tag>
          <el-tag type="danger">未拣选数量: {{summary.unpickedQuantity}}</el-tag>
          <el-tag type="info">托盘状态: {{palletStatus}}</el-tag>
        </div>
      </el-card>
    </div>
    <!-- æ•°æ®åˆ—表 -->
    <div class="content-area">
      <el-row :gutter="20">
        <el-col :span="12">
          <el-card header="未拣选列表">
            <el-table :data="unpickedList" border height="440">
              <el-table-column prop="materielCode" label="物料编码" width="120"></el-table-column>
              <el-table-column prop="assignQuantity" label="分配数量" width="100"></el-table-column>
              <el-table-column prop="pickedQty" label="已拣数量" width="100"></el-table-column>
              <el-table-column prop="remainQuantity" label="剩余数量" width="100"></el-table-column>
              <el-table-column prop="locationCode" label="货位" width="100"></el-table-column>
              <el-table-column prop="currentBarcode" label="条码"></el-table-column>
            </el-table>
          </el-card>
        </el-col>
        <el-col :span="12">
          <el-card header="已拣选列表">
            <div class="table-actions">
              <el-button
                size="mini"
                type="danger"
                :disabled="selectedPickedRows.length === 0"
                @click="batchCancelSelected">
                å–消拣选
              </el-button>
              <span class="selection-count">已选择 {{selectedPickedRows.length}} é¡¹</span>
            </div>
            <el-table
              :data="pickedList"
              border
              height="400"
              style="width: 100%"
              @selection-change="handlePickedSelectionChange">
              <el-table-column type="selection" width="55"></el-table-column>
              <el-table-column prop="materielCode" label="物料编码" width="120"></el-table-column>
              <el-table-column prop="pickedQty" label="已拣数量" width="100"></el-table-column>
              <el-table-column prop="locationCode" label="货位" width="100"></el-table-column>
              <el-table-column prop="currentBarcode" label="条码"></el-table-column>
            </el-table>
          </el-card>
        </el-col>
      </el-row>
    </div>
    <!-- åˆ†æ‰¹åˆ†é…å¼¹çª— -->
    <div v-if="showBatchAllocateDialog" class="custom-dialog-overlay">
      <div class="custom-dialog-wrapper">
        <div class="custom-dialog">
          <div class="custom-dialog-header">
            <h3>分批分配库存</h3>
            <el-button type="text" @click="closeBatchAllocateDialog" class="close-button">×</el-button>
          </div>
          <div class="custom-dialog-body">
            <el-form :model="batchAllocateForm" :rules="batchAllocateFormRules" ref="batchAllocateFormRef" label-width="100px">
              <el-form-item label="订单编号">
                <el-input v-model="batchAllocateForm.orderNo" disabled></el-input>
              </el-form-item>
              <el-form-item label="物料明细" prop="orderDetailId">
                <el-select v-model="batchAllocateForm.orderDetailId" placeholder="选择物料明细" style="width: 100%">
                  <el-option
                    v-for="detail in allocatableDetails"
                    :key="detail.id"
                    :label="`${detail.materielCode} - éœ€æ±‚:${detail.needOutQuantity} å·²åˆ†é…:${detail.allocatedQuantity} å¯åˆ†é…:${detail.availableQuantity}`"
                    :value="detail.id">
                  </el-option>
                </el-select>
              </el-form-item>
              <el-form-item label="分配数量" prop="batchQuantity">
                <el-input-number
                  v-model="batchAllocateForm.batchQuantity"
                  :min="0.01"
                  :precision="2"
                  :step="1"
                  style="width: 100%"
                  placeholder="输入分配数量">
                </el-input-number>
              </el-form-item>
              <el-form-item label="可分配数量">
                <el-input :value="getAvailableQuantity()" disabled></el-input>
              </el-form-item>
            </el-form>
          </div>
          <div class="custom-dialog-footer">
            <el-button @click="closeBatchAllocateDialog">取消</el-button>
            <el-button type="primary" @click="handleBatchAllocate" :loading="batchAllocateLoading">确认分配</el-button>
          </div>
        </div>
      </div>
    </div>
    <!-- æ‹†åŒ…弹窗 -->
    <div v-if="showCustomSplitDialog" class="custom-dialog-overlay">
      <div class="custom-dialog-wrapper">
        <div class="custom-dialog">
          <div class="custom-dialog-header">
            <h3>拆包操作</h3>
            <el-button type="text" @click="closeCustomSplitDialog" class="close-button">X</el-button>
          </div>
          <div class="custom-dialog-body">
            <el-form :model="splitForm" :rules="splitFormRules" ref="splitFormRef" label-width="100px">
              <el-form-item label="订单编号">
                <el-input v-model="splitForm.orderNo" disabled></el-input>
              </el-form-item>
              <el-form-item label="批次编号">
                <el-input v-model="splitForm.batchNo" disabled></el-input>
              </el-form-item>
              <el-form-item label="托盘编号">
                <el-input v-model="splitForm.palletCode" disabled></el-input>
              </el-form-item>
              <el-form-item label="原条码" prop="originalBarcode">
                <el-input
                  v-model="splitForm.originalBarcode"
                  placeholder="扫描原条码"
                  @keyup.enter.native="onSplitBarcodeScan"
                  @change="onSplitBarcodeScan"
                  clearable>
                </el-input>
              </el-form-item>
              <el-form-item label="物料编码">
                <el-input v-model="splitForm.materielCode" disabled></el-input>
              </el-form-item>
              <el-form-item label="剩余数量">
                <el-input v-model="splitForm.maxQuantity" disabled></el-input>
              </el-form-item>
              <el-form-item label="拆包数量" prop="splitQuantity">
                <el-input-number
                  v-model="splitForm.splitQuantity"
                  :min="1"
                  :max="splitForm.maxQuantity"
                  :precision="2"
                  :step="1"
                  style="width: 100%">
                </el-input-number>
              </el-form-item>
            </el-form>
          </div>
          <div class="custom-dialog-footer">
            <el-button @click="closeCustomSplitDialog">取消</el-button>
            <el-button type="primary" @click="handleSplitPackage" :loading="splitLoading">确认拆包</el-button>
          </div>
        </div>
      </div>
    </div>
    <!-- æ’¤é”€æ‹†åŒ…弹窗 -->
    <div v-if="showRevertSplitDialog" class="custom-dialog-overlay">
      <div class="custom-dialog-wrapper">
        <div class="custom-dialog">
          <div class="custom-dialog-header">
            <h3>撤销拆包</h3>
            <el-button type="text" @click="closeRevertSplitDialog" class="close-button">×</el-button>
          </div>
          <div class="custom-dialog-body">
            <el-form :model="revertSplitForm" :rules="revertSplitFormRules" ref="revertSplitFormRef" label-width="100px">
              <el-form-item label="新条码" prop="newBarcode">
                <el-input
                  v-model="revertSplitForm.newBarcode"
                  placeholder="扫描新条码"
                  @keyup.enter.native="onRevertSplitBarcodeScan"
                  @change="onRevertSplitBarcodeScan"
                  clearable>
                </el-input>
              </el-form-item>
            </el-form>
          </div>
          <div class="custom-dialog-footer">
            <el-button @click="closeRevertSplitDialog">取消</el-button>
            <el-button type="primary" @click="handleRevertSplit" :loading="revertSplitLoading">确认撤销</el-button>
          </div>
        </div>
      </div>
    </div>
    <!-- æ‰¹é‡å›žåº“弹窗 -->
    <div v-if="showBatchReturnDialog" class="custom-dialog-overlay">
      <div class="custom-dialog-wrapper">
        <div class="custom-dialog">
          <div class="custom-dialog-header">
            <h3>批次回库</h3>
            <el-button type="text" @click="closeBatchReturnDialog" class="close-button">×</el-button>
          </div>
          <div class="custom-dialog-body">
            <el-form :model="batchReturnForm" :rules="batchReturnFormRules" ref="batchReturnFormRef" label-width="100px">
              <el-form-item label="订单编号">
                <el-input v-model="batchReturnForm.orderNo" disabled></el-input>
              </el-form-item>
              <el-form-item label="批次编号">
                <el-input v-model="batchReturnForm.batchNo" disabled></el-input>
              </el-form-item>
              <el-form-item label="未拣选数量">
                <el-input v-model="batchReturnForm.unpickedQuantity" disabled></el-input>
              </el-form-item>
              <el-form-item label="未拣选条数">
                <el-input v-model="batchReturnForm.unpickedCount" disabled></el-input>
              </el-form-item>
            </el-form>
          </div>
          <div class="custom-dialog-footer">
            <el-button @click="closeBatchReturnDialog">取消</el-button>
            <el-button type="primary" @click="handleBatchReturnConfirm" :loading="batchReturnLoading">确认回库</el-button>
          </div>
        </div>
      </div>
    </div>
    <!-- å–走空箱弹窗 -->
    <div v-if="showEmptyPalletDialog" class="custom-dialog-overlay">
      <div class="custom-dialog-wrapper">
        <div class="custom-dialog">
          <div class="custom-dialog-header">
            <h3>取走空箱</h3>
            <el-button type="text" @click="closeEmptyPalletDialog" class="close-button">×</el-button>
          </div>
          <div class="custom-dialog-body">
            <el-form :model="emptypalletOutForm" :rules="emptypalletOutFormRules" ref="emptypalletOutFormRef" label-width="100px">
              <el-form-item label="订单编号">
                <el-input v-model="emptypalletOutForm.orderNo" disabled></el-input>
              </el-form-item>
              <el-form-item label="托盘编号" prop="palletCode">
                <el-input
                  v-model="emptypalletOutForm.palletCode"
                  placeholder="扫描托盘码"
                  @keyup.enter.native="onEmptyPalletScan"
                  @change="onEmptyPalletScan"
                  clearable>
                </el-input>
              </el-form-item>
            </el-form>
          </div>
          <div class="custom-dialog-footer">
            <el-button @click="closeEmptyPalletDialog">取消</el-button>
            <el-button type="primary" @click="handleEmptyPalletConfirm" :loading="emptypalletOutLoading">确认取走空箱</el-button>
          </div>
        </div>
      </div>
    </div>
    <print-view ref="childs" @parentcall="parentcall"></print-view>
  </div>
</template>
<script>
import http from '@/api/http.js'
import { ref, defineComponent } from "vue";
import { ElMessage } from 'element-plus'
import { useRoute } from 'vue-router'
import printView from "@/extension/outbound/extend/printView.vue"
export default defineComponent({
  name: 'BatchOutboundPicking',
  components: {printView},
  data() {
    // éªŒè¯è§„则定义...
    const validateBatchQuantity = (rule, value, callback) => {
      if (value === null || value === undefined || value === '') {
        callback(new Error('请输入分配数量'));
      } else if (value <= 0) {
        callback(new Error('分配数量必须大于0'));
      } else {
        callback();
      }
    };
    const validateOrderDetailId = (rule, value, callback) => {
      if (!value) {
        callback(new Error('请选择物料明细'));
      } else {
        callback();
      }
    };
    return {
      scanData: {
        orderNo: '',
        palletCode: '',
        barcode: '',
        batchNo: ''
      },
      currentBatchNo: '', // å½“前批次号
      batchList: [], // æ‰¹æ¬¡åˆ—表
      selectedBatchNo: '', // é€‰ä¸­çš„æ‰¹æ¬¡å·
      batchSummary: {}, // æ‰¹æ¬¡æ±‡æ€»ä¿¡æ¯
      unpickedList: [],
      pickedList: [],
      selectedPickedRows: [],
      summary: {
        unpickedCount: 0,
        unpickedQuantity: 0,
        pickedCount: 0
      },
      palletStatus: '未知',
      // å¼¹çª—状态
      showBatchAllocateDialog: false,
      showCustomSplitDialog: false,
      showRevertSplitDialog: false,
      showBatchReturnDialog: false,
      showEmptyPalletDialog: false,
      // åŠ è½½çŠ¶æ€
      batchAllocateLoading: false,
      splitLoading: false,
      revertSplitLoading: false,
      batchReturnLoading: false,
      emptypalletOutLoading: false,
      // è¡¨å•数据
      batchAllocateForm: {
        orderNo: '',
        orderDetailId: '',
        batchQuantity: 0
      },
      allocatableDetails: [], // å¯åˆ†é…çš„订单明细
      splitForm: {
        orderNo: '',
        batchNo: '',
        palletCode: '',
        originalBarcode: '',
        materielCode: '',
        splitQuantity: 0,
        maxQuantity: 0
      },
      revertSplitForm: {
        newBarcode: ''
      },
      batchReturnForm: {
        orderNo: '',
        batchNo: '',
        unpickedCount: 0,
        unpickedQuantity: 0
      },
      emptypalletOutForm: {
        orderNo: '',
        palletCode: ''
      },
      // éªŒè¯è§„则
      batchAllocateFormRules: {
        orderDetailId: [
          { required: true, validator: validateOrderDetailId, trigger: 'change' }
        ],
        batchQuantity: [
          { required: true, validator: validateBatchQuantity, trigger: 'blur' }
        ]
      },
      // å…¶ä»–验证规则...
      isProcessing: false
    }
  },
  mounted() {
    if (this.$route.query.orderNo) {
      this.scanData.orderNo = this.$route.query.orderNo;
      this.batchAllocateForm.orderNo = this.$route.query.orderNo;
      this.loadBatchList();
    }
    this.$nextTick(() => {
      this.$refs.palletInput.focus();
    });
  },
  methods: {
    goBack(){
      this.$router.back()
    },
    // æ‰¹æ¬¡ç›¸å…³æ–¹æ³•
    async loadBatchList() {
      try {
        const res = await http.post('/api/BatchOutbound/order-batch-list', {
          orderNo: this.scanData.orderNo
        });
        if (res.status) {
          this.batchList = res.data || [];
          if (this.batchList.length > 0) {
            this.selectedBatchNo = this.batchList[0].batchNo;
            this.currentBatchNo = this.selectedBatchNo;
            this.scanData.batchNo = this.selectedBatchNo;
            this.loadBatchData();
          }
        }
      } catch (error) {
        this.$message.error('加载批次列表失败');
      }
    },
    async refreshBatchList() {
      await this.loadBatchList();
      this.$message.success('批次列表已刷新');
    },
    onBatchChange(batchNo) {
      this.currentBatchNo = batchNo;
      this.scanData.batchNo = batchNo;
      this.loadBatchData();
    },
    async loadBatchData() {
      if (!this.currentBatchNo) return;
      await this.loadBatchSummary();
      await this.loadUnpickedList();
      await this.loadPickedList();
    },
    async loadBatchSummary() {
      try {
        const res = await http.post('/api/BatchOutbound/batch-summary', {
          orderNo: this.scanData.orderNo,
          batchNo: this.currentBatchNo
        });
        if (res.status) {
          this.batchSummary = res.data || {};
        }
      } catch (error) {
        this.$message.error('加载批次汇总失败');
      }
    },
    async loadUnpickedList() {
      try {
        const res = await http.post('/api/BatchOutbound/batch-unpicked-list', {
          orderNo: this.scanData.orderNo,
          batchNo: this.currentBatchNo
        });
        this.unpickedList = res.data || [];
        this.summary.unpickedCount = this.unpickedList.length;
        this.summary.unpickedQuantity = this.unpickedList.reduce((sum, item) => sum + (item.remainQuantity || 0), 0);
      } catch (error) {
        this.$message.error('加载未拣选列表失败');
      }
    },
    async loadPickedList() {
      try {
        const res = await http.post('/api/BatchOutbound/batch-picked-list', {
          orderNo: this.scanData.orderNo,
          batchNo: this.currentBatchNo
        });
        this.pickedList = res.data || [];
        this.summary.pickedCount = this.pickedList.length;
      } catch (error) {
        this.$message.error('加载已拣选列表失败');
      }
    },
    getBatchStatusType(status) {
      const statusMap = {
        0: 'info', // åˆ†é…ä¸­
        1: 'warning', // æ‰§è¡Œä¸­
        2: 'success', // å·²å®Œæˆ
        3: 'danger' // å·²å›žåº“
      };
      return statusMap[status] || 'info';
    },
    // åˆ†æ‰¹åˆ†é…ç›¸å…³æ–¹æ³•
    async openBatchAllocateDialog() {
      this.showBatchAllocateDialog = true;
      await this.loadAllocatableDetails();
      this.batchAllocateForm.orderDetailId = '';
      this.batchAllocateForm.batchQuantity = 0;
    },
    async loadAllocatableDetails() {
      try {
        const res = await http.post('/api/BatchOutbound/allocatable-order-details', {
          orderNo: this.scanData.orderNo
        });
        if (res.status) {
          this.allocatableDetails = res.data || [];
        }
      } catch (error) {
        this.$message.error('加载可分配明细失败');
      }
    },
    getAvailableQuantity() {
      const detail = this.allocatableDetails.find(d => d.id === this.batchAllocateForm.orderDetailId);
      return detail ? detail.availableQuantity : 0;
    },
    async handleBatchAllocate() {
      if (this.$refs.batchAllocateFormRef) {
        this.$refs.batchAllocateFormRef.validate(async (valid) => {
          if (valid) {
            this.batchAllocateLoading = true;
            try {
              const res = await http.post('/api/BatchOutbound/batch-allocate-stock', this.batchAllocateForm);
              if (res.status) {
                this.$message.success('分批分配成功');
                this.showBatchAllocateDialog = false;
                await this.loadBatchList(); // åˆ·æ–°æ‰¹æ¬¡åˆ—表
              } else {
                this.$message.error(res.message || '分批分配失败');
              }
            } catch (error) {
              this.$message.error('分批分配失败');
            } finally {
              this.batchAllocateLoading = false;
            }
          }
        });
      }
    },
    closeBatchAllocateDialog() {
      this.showBatchAllocateDialog = false;
    },
    // åˆ†æ‹£ç›¸å…³æ–¹æ³•
    async confirmPicking() {
      if (this.isProcessing) return;
      if (!this.scanData.orderNo || !this.scanData.palletCode || !this.scanData.barcode) {
        this.$message.warning('请先扫描托盘码和物料条码');
        this.focusBarcodeInput();
        return;
      }
      if (!this.currentBatchNo) {
        this.$message.warning('请先选择批次');
        return;
      }
      this.isProcessing = true;
      try {
        const res = await http.post('/api/BatchOutbound/confirm-picking', this.scanData);
        if (res.status) {
          this.$message.success('拣选确认成功');
          this.scanData.barcode = '';
          await this.loadBatchData();
          if(res.data && res.data.splitResults && res.data.splitResults.length>0){
            this.$refs.childs.open(res.data.splitResults);
          }
          this.$nextTick(() => {
            this.$refs.barcodeInput.focus();
          });
        } else {
          this.$message.error(res.message || '拣选确认失败');
          this.focusBarcodeInput(true);
        }
      } catch (error) {
        this.$message.error('拣选确认失败: ' + (error.message || '网络错误'));
        this.focusBarcodeInput(true);
      } finally {
        this.isProcessing = false;
      }
    },
    // æ‹†åŒ…相关方法
    openSplitDialog() {
      if (!this.scanData.palletCode) {
        this.$message.warning('请先扫描托盘码');
        return;
      }
      if (!this.currentBatchNo) {
        this.$message.warning('请先选择批次');
        return;
      }
      this.showCustomSplitDialog = true;
      this.resetSplitForm();
      this.splitForm.orderNo = this.scanData.orderNo;
      this.splitForm.batchNo = this.currentBatchNo;
      this.splitForm.palletCode = this.scanData.palletCode;
    },
    async onSplitBarcodeScan() {
      if (!this.splitForm.originalBarcode) return;
      this.splitForm.originalBarcode = this.splitForm.originalBarcode.replace(/\n/g, '').trim();
      try {
        const res = await http.post('/api/BatchOutbound/split-package-info', {
          orderNo: this.splitForm.orderNo,
          batchNo: this.splitForm.batchNo,
          barcode: this.splitForm.originalBarcode
        });
        if (res.status) {
          this.splitForm.materielCode = res.data.materielCode;
          this.splitForm.maxQuantity = res.data.remainQuantity;
          this.splitForm.splitQuantity = Math.min(1, this.splitForm.maxQuantity);
        } else {
          this.$message.error(res.message || '获取拆包信息失败');
        }
      } catch (error) {
        this.$message.error('获取拆包信息失败');
      }
    },
    async handleSplitPackage() {
      if (this.$refs.splitFormRef) {
        this.$refs.splitFormRef.validate(async (valid) => {
          if (valid) {
            this.splitLoading = true;
            try {
              const res = await http.post('/api/BatchOutbound/manual-split-package', this.splitForm);
              if (res.status) {
                this.$message.success('拆包成功');
                this.showCustomSplitDialog = false;
                await this.loadBatchData();
              } else {
                this.$message.error(res.message || '拆包失败');
              }
            } catch (error) {
              this.$message.error('拆包失败');
            } finally {
              this.splitLoading = false;
            }
          }
        });
      }
    },
    // æ’¤é”€æ‹†åŒ…
    async onRevertSplitBarcodeScan() {
      if (!this.revertSplitForm.newBarcode) return;
      this.revertSplitForm.newBarcode = this.revertSplitForm.newBarcode.replace(/\n/g, '').trim();
    },
    async handleRevertSplit() {
      if (this.$refs.revertSplitFormRef) {
        this.$refs.revertSplitFormRef.validate(async (valid) => {
          if (valid) {
            this.revertSplitLoading = true;
            try {
              const res = await http.post('/api/BatchOutbound/cancel-split-package', {
                orderNo: this.scanData.orderNo,
                batchNo: this.currentBatchNo,
                newBarcode: this.revertSplitForm.newBarcode
              });
              if (res.status) {
                this.$message.success('撤销拆包成功');
                this.showRevertSplitDialog = false;
                await this.loadBatchData();
              } else {
                this.$message.error(res.message || '撤销拆包失败');
              }
            } catch (error) {
              this.$message.error('撤销拆包失败');
            } finally {
              this.revertSplitLoading = false;
            }
          }
        });
      }
    },
    // å›žåº“相关方法
    openBatchReturnDialog() {
      if (!this.currentBatchNo) {
        this.$message.warning('请先选择批次');
        return;
      }
      this.showBatchReturnDialog = true;
      this.batchReturnForm.orderNo = this.scanData.orderNo;
      this.batchReturnForm.batchNo = this.currentBatchNo;
      this.batchReturnForm.unpickedCount = this.summary.unpickedCount;
      this.batchReturnForm.unpickedQuantity = this.summary.unpickedQuantity;
    },
    async handleBatchReturnConfirm() {
      this.batchReturnLoading = true;
      try {
        const res = await http.post('/api/BatchOutbound/batch-return-stock', {
          orderNo: this.scanData.orderNo,
          batchNo: this.currentBatchNo
        });
        if (res.status) {
          this.$message.success('批次回库成功');
          this.showBatchReturnDialog = false;
          await this.loadBatchData();
        } else {
          this.$message.error(res.message || '批次回库失败');
        }
      } catch (error) {
        this.$message.error('批次回库失败');
      } finally {
        this.batchReturnLoading = false;
      }
    },
    // å–空箱方法
    async handleEmptyPalletConfirm() {
      this.emptypalletOutLoading = true;
      try {
        const res = await http.post('/api/BatchOutbound/remove-empty-pallet', this.emptypalletOutForm);
        if (res.status) {
          this.$message.success('取走空箱成功');
          this.showEmptyPalletDialog = false;
          await this.loadBatchData();
        } else {
          this.$message.error(res.message || '取走空箱失败');
        }
      } catch (error) {
        this.$message.error('取走空箱失败');
      } finally {
        this.emptypalletOutLoading = false;
      }
    },
    // å…¶ä»–原有方法...
    onPalletScan() {
      this.scanData.palletCode = this.scanData.palletCode.replace(/\n/g, '').trim();
      if (!this.scanData.palletCode) return;
      this.loadActiveBatch();
      this.$nextTick(() => {
        this.$refs.barcodeInput.focus();
      });
    },
    async loadActiveBatch() {
      try {
        const res = await http.post('/api/BatchOutbound/active-batch', {
          orderNo: this.scanData.orderNo,
          palletCode: this.scanData.palletCode
        });
        if (res.status && res.data) {
          this.currentBatchNo = res.data.batchNo;
          this.scanData.batchNo = res.data.batchNo;
          this.selectedBatchNo = res.data.batchNo;
          await this.loadBatchData();
        }
      } catch (error) {
        console.log('获取活跃批次失败,可能托盘没有关联批次');
      }
    },
    onBarcodeScan() {
      this.scanData.barcode = this.scanData.barcode.replace(/\n/g, '').trim();
      if (!this.scanData.barcode) return;
      this.confirmPicking();
    },
    focusBarcodeInput(selectText = false) {
      this.$nextTick(() => {
        const input = this.$refs.barcodeInput;
        if (input && input.$el && input.$el.querySelector('input')) {
          const inputEl = input.$el.querySelector('input');
          inputEl.focus();
          if (selectText) {
            inputEl.select();
          }
        }
      });
    },
    handlePickedSelectionChange(selection) {
      this.selectedPickedRows = selection;
    },
    async batchCancelSelected() {
      if (this.selectedPickedRows.length === 0) {
        this.$message.warning('请先选择要取消的项');
        return;
      }
      this.$confirm(`确定要取消选中的 ${this.selectedPickedRows.length} é¡¹å—?`, '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(async () => {
        try {
          for (const row of this.selectedPickedRows) {
            try {
              const res = await http.post('/api/BatchOutbound/cancel-picking', {
                orderNo: this.scanData.orderNo,
                batchNo: this.currentBatchNo,
                palletCode: this.scanData.palletCode,
                barcode: row.currentBarcode
              });
              if (!res.status) {
                this.$message.warning(`取消拣选失败: ${row.currentBarcode} - ${res.message}`);
              }
            } catch (error) {
              this.$message.warning(`取消拣选失败: ${row.currentBarcode} - ${error.message}`);
            }
          }
          this.$message.success('批量取消完成');
          await this.loadBatchData();
          this.selectedPickedRows = [];
        } catch (error) {
          this.$message.error('批量取消操作失败');
        }
      });
    },
    // é‡ç½®æ–¹æ³•
    resetSplitForm() {
      this.splitForm.originalBarcode = '';
      this.splitForm.materielCode = '';
      this.splitForm.splitQuantity = 0;
      this.splitForm.maxQuantity = 0;
    },
    closeCustomSplitDialog() {
      this.showCustomSplitDialog = false;
      this.resetSplitForm();
    },
    openRevertSplitDialog() {
      this.showRevertSplitDialog = true;
      this.revertSplitForm.newBarcode = '';
    },
    closeRevertSplitDialog() {
      this.showRevertSplitDialog = false;
      this.revertSplitForm.newBarcode = '';
    },
    closeBatchReturnDialog() {
      this.showBatchReturnDialog = false;
    },
    openEmptyPalletDialog() {
      this.showEmptyPalletDialog = true;
      this.emptypalletOutForm.orderNo = this.scanData.orderNo;
      this.emptypalletOutForm.palletCode = '';
    },
    closeEmptyPalletDialog() {
      this.showEmptyPalletDialog = false;
      this.emptypalletOutForm.palletCode = '';
    },
    onEmptyPalletScan() {
      if (!this.emptypalletOutForm.palletCode) return;
      this.emptypalletOutForm.palletCode = this.emptypalletOutForm.palletCode.replace(/\n/g, '').trim();
    }
  }
})
</script>
<style scoped>
.OutboundPicking-container {
  padding: 20px;
}
.batch-operations {
  margin-bottom: 15px;
}
.batch-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}
.batch-summary-area {
  margin-bottom: 15px;
}
.batch-summary-info {
  display: flex;
  gap: 15px;
  flex-wrap: wrap;
}
.scanner-form {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
}
.scanner-form .el-input {
  width: 200px;
}
.summary-info {
  display: flex;
  gap: 20px;
  flex-wrap: wrap;
}
.table-actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
  padding: 0 10px;
}
.selection-count {
  font-size: 12px;
  color: #909399;
}
/* è‡ªå®šä¹‰å¼¹çª—样式 */
.custom-dialog-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
}
.custom-dialog-wrapper {
  position: relative;
  z-index: 10000;
}
.custom-dialog {
  background: white;
  border-radius: 4px;
  width: 500px;
  max-width: 90vw;
  max-height: 90vh;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  overflow: auto;
}
.custom-dialog-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px 20px 10px;
  border-bottom: 1px solid #ebeef5;
}
.custom-dialog-header h3 {
  margin: 0;
  color: #303133;
}
.close-button {
  font-size: 18px;
  color: #909399;
  padding: 0;
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.close-button:hover {
  color: #409EFF;
  background-color: transparent;
}
.custom-dialog-body {
  padding: 20px;
}
.custom-dialog-footer {
  padding: 10px 20px 20px;
  text-align: right;
  border-top: 1px solid #ebeef5;
}
.custom-dialog-footer .el-button {
  margin-left: 10px;
}
@media (max-width: 768px) {
  .custom-dialog {
    width: 95vw;
    margin: 10px;
  }
  .scanner-form {
    flex-direction: column;
    align-items: stretch;
  }
  .scanner-form .el-input {
    width: 100%;
  }
}
</style>
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/17.14.878.3237/CodeChunks.db-shm
Binary files differ
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/17.14.878.3237/SemanticSymbols.db-shm
Binary files differ
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Common/OrderEnum/OutboundOrderEnum.cs
@@ -21,12 +21,18 @@
        [Description("出库中")]
        å‡ºåº“中 = 1,
        /// <summary>
        /// å‡ºåº“完成
        /// </summary>
        [Description("出库完成")]
        å‡ºåº“完成 = 2,
        [Description("部分完成")]
        éƒ¨åˆ†å®Œæˆ =3,
        /// <summary>
        /// å…³é—­
        /// </summary>
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Common/StockEnum/OutLockStockStatusEnum.cs
@@ -24,7 +24,8 @@
        æ‰§è¡Œä¸­ = 1,
        å·²å®Œæˆ = 2,
        å·²å›žåº“ = 3,
        å·²å–消 = 4
        å·²å–消 = 4,
    }
    public enum SplitPackageStatusEnum
@@ -63,6 +64,9 @@
        [Description("已回库")]
        å·²å›žåº“ =8,
        [Description("已释放")]
        å·²é‡Šæ”¾ =9,
        [Description("撤销")]
        æ’¤é”€ = 99
    }
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_IOutboundService/IOutboundBatchPickingService.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
using WIDESEA_Core;
using WIDESEA_Core.BaseRepository;
using WIDESEA_Model.Models;
namespace WIDESEA_IOutboundService
{
    public interface IOutboundBatchPickingService
    {
        IRepository<Dt_PickingRecord> Repository { get; }
        Task<WebResponseContent> BatchReturnStock(string orderNo, string palletCode);
        Task<WebResponseContent> CancelPicking(string orderNo, string palletCode, string barcode);
        Task<WebResponseContent> CancelSplitPackage(string orderNo, string palletCode, string newBarcode);
        Task<WebResponseContent> ConfirmBatchPicking(string orderNo, string palletCode, string barcode);
        Task<WebResponseContent> ManualSplitPackage(string orderNo, string palletCode, string originalBarcode, decimal splitQuantity);
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_InboundService/InboundOrderService.cs
@@ -96,7 +96,7 @@
                        item.Unit = purchaseToStockResult.Unit;
                        item.OrderQuantity = purchaseToStockResult.Quantity;
                    }
                    if (model.OrderType != InOrderTypeEnum.Allocat.ObjToInt())
                    if (model.OrderType != InOrderTypeEnum.AllocatInbound.ObjToInt())
                    {
                        model.InboundOrderNo = CreateCodeByRule(nameof(RuleCodeEnum.InboundOrderRule));
                    }
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Model/Models/Outbound/Dt_PickingRecord.cs
@@ -44,7 +44,13 @@
        public int StockId { get; set; }
        public string BatchNo { get; set; }
        public bool IsCancelled { get; set; }
        public DateTime? CancelTime { get; set; }
        public string CancelOperator { get; set; }
        public string FactoryArea { get; set; }
    }
@@ -107,6 +113,7 @@
        public DateTime RevertTime { get; set; }
        public string RevertOperator { get; set; }
        public int PreviousSplitRecordId { get; set; }
        [SugarColumn(IsNullable = true)]
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundBatchPickingService.cs
@@ -21,7 +21,7 @@
namespace WIDESEA_OutboundService
{
    public  class OutboundBatchPickingService : ServiceBase<Dt_PickingRecord, IRepository<Dt_PickingRecord>>
    public class OutboundBatchPickingService : ServiceBase<Dt_PickingRecord, IRepository<Dt_PickingRecord>>, IOutboundBatchPickingService
    {
 
@@ -86,72 +86,327 @@
        /// <summary>
        /// åˆ†æ‰¹åˆ†æ‹£ç¡®è®¤
        /// </summary>
        public async Task<WebResponseContent> ConfirmBatchPicking(string orderNo, string batchNo, string palletCode, string barcode, decimal actualPickedQty)
        public async Task<WebResponseContent> ConfirmBatchPicking(string orderNo, string palletCode, string barcode)
        {
            try
            {
                _unitOfWorkManage.BeginTran();
                // 1. éªŒè¯åˆ†æ‹£è¯·æ±‚
                var validationResult = await ValidateBatchPickingRequest(orderNo, batchNo, palletCode, barcode, actualPickedQty);
                var validationResult = await ValidatePickingRequest(orderNo, palletCode, barcode);
                if (!validationResult.IsValid)
                    return WebResponseContent.Instance.Error(validationResult.ErrorMessage);
                var (lockInfo, orderDetail, stockDetail) = validationResult.Data;
                var (lockInfo, orderDetail, stockDetail, batch) = validationResult.Data;
                // ä½¿ç”¨é”å®šä¿¡æ¯çš„分配数量作为实际分拣数量
                var actualPickedQty = lockInfo.AssignQuantity;
                // 2. æ‰§è¡Œåˆ†æ‹£é€»è¾‘
                var pickingResult = await ExecuteBatchPickingLogic(lockInfo, orderDetail, stockDetail, actualPickedQty);
                var pickingResult = await ExecutePickingLogic(lockInfo, orderDetail, stockDetail, actualPickedQty);
                // 3. æ›´æ–°æ‰¹æ¬¡å®Œæˆæ•°é‡
                await UpdateBatchCompletedQuantity(batchNo, actualPickedQty);
                // 3. æ›´æ–°æ‰¹æ¬¡å’Œè®¢å•数据
                await UpdateBatchAndOrderData(batch, orderDetail, actualPickedQty, orderNo);
                // 4. æ›´æ–°è®¢å•相关数据
                await UpdateOrderRelatedData(orderDetail.Id, actualPickedQty, orderNo);
                // 5. è®°å½•拣选历史
                await RecordPickingHistory(pickingResult, orderNo, palletCode, batchNo);
                // 4. è®°å½•拣选历史
                await RecordPickingHistory(pickingResult, orderNo, palletCode);
                _unitOfWorkManage.CommitTran();
                return WebResponseContent.Instance.OK("分批分拣成功");
                return WebResponseContent.Instance.OK("分拣成功", new
                {
                    PickedQuantity = actualPickedQty,
                    Barcode = barcode,
                    MaterialCode = lockInfo.MaterielCode
                });
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                _logger.LogError($"分批分拣失败 - OrderNo: {orderNo}, BatchNo: {batchNo}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error($"分批分拣失败:{ex.Message}");
                _logger.LogError($"分拣失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Barcode: {barcode}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error($"分拣失败:{ex.Message}");
            }
        }
        /// <summary>
        /// å–消分拣
        /// </summary>
        public async Task<WebResponseContent> CancelPicking(string orderNo, string palletCode, string barcode)
        {
            try
            {
                _unitOfWorkManage.BeginTran();
                // æŸ¥æ‰¾åˆ†æ‹£è®°å½•
                var pickingRecord = await Db.Queryable<Dt_PickingRecord>()
                    .Where(x => x.OrderNo == orderNo &&
                               x.PalletCode == palletCode &&
                               x.Barcode == barcode &&
                               !x.IsCancelled)
                    .OrderByDescending(x => x.PickTime)
                    .FirstAsync();
                if (pickingRecord == null)
                    return WebResponseContent.Instance.Error("未找到分拣记录");
                // æ¢å¤é”å®šä¿¡æ¯å’Œåº“å­˜
                await RevertPickingData(pickingRecord);
                //更新批次和订单数据
                await RevertBatchAndOrderData(pickingRecord);
                // æ ‡è®°åˆ†æ‹£è®°å½•为已取消
                pickingRecord.IsCancelled = true;
                pickingRecord.CancelTime = DateTime.Now;
                pickingRecord.CancelOperator = App.User.UserName;
                await Db.Updateable(pickingRecord).ExecuteCommandAsync();
                _unitOfWorkManage.CommitTran();
                return WebResponseContent.Instance.OK("取消分拣成功");
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                _logger.LogError($"取消分拣失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error($"取消分拣失败:{ex.Message}");
            }
        }
        private async Task<ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail)>> ValidateBatchPickingRequest(
            string orderNo, string batchNo, string palletCode, string barcode, decimal actualPickedQty)
        #endregion
        #region æ‰‹åŠ¨æ‹†åŒ…
        /// <summary>
        /// æ‰‹åŠ¨æ‹†åŒ…
        /// </summary>
        public async Task<WebResponseContent> ManualSplitPackage(string orderNo, string palletCode, string originalBarcode, decimal splitQuantity)
        {
            // æŸ¥æ‰¾æ‰¹æ¬¡é”å®šä¿¡æ¯
            try
            {
                _unitOfWorkManage.BeginTran();
                //  éªŒè¯æ‹†åŒ…请求
                var validationResult = await ValidateSplitRequest(orderNo, palletCode, originalBarcode, splitQuantity);
                if (!validationResult.IsValid)
                    return WebResponseContent.Instance.Error(validationResult.ErrorMessage);
                var (lockInfo, stockDetail) = validationResult.Data;
                // . æ‰§è¡Œæ‹†åŒ…逻辑
                var splitResult = await ExecuteSplitLogic(lockInfo, stockDetail, splitQuantity, palletCode);
                _unitOfWorkManage.CommitTran();
                return WebResponseContent.Instance.OK("手动拆包成功", new
                {
                    NewBarcode = splitResult.NewBarcode,
                    OriginalBarcode = originalBarcode,
                    SplitQuantity = splitQuantity
                });
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                _logger.LogError($"手动拆包失败 - OrderNo: {orderNo}, Barcode: {originalBarcode}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error($"手动拆包失败:{ex.Message}");
            }
        }
        #endregion
        #region å–消拆包
        /// <summary>
        /// å–消拆包
        /// </summary>
        public async Task<WebResponseContent> CancelSplitPackage(string orderNo, string palletCode, string newBarcode)
        {
            try
            {
                _unitOfWorkManage.BeginTran();
                // æŸ¥æ‰¾æ‹†åŒ…记录并验证
                var validationResult = await ValidateCancelSplitRequest(orderNo, palletCode, newBarcode);
                if (!validationResult.IsValid)
                    return WebResponseContent.Instance.Error(validationResult.ErrorMessage);
                var (splitRecord, newLockInfo, newStockDetail) = validationResult.Data;
                // æ‰§è¡Œå–消拆包逻辑
                await ExecuteCancelSplitLogic(splitRecord, newLockInfo, newStockDetail);
                _unitOfWorkManage.CommitTran();
                return WebResponseContent.Instance.OK("取消拆包成功");
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                _logger.LogError($"取消拆包失败 - OrderNo: {orderNo}, Barcode: {newBarcode}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error($"取消拆包失败:{ex.Message}");
            }
        }
        #endregion
        #region åˆ†æ‰¹å›žåº“
        /// <summary>
        /// åˆ†æ‰¹å›žåº“ - é‡Šæ”¾æœªæ‹£é€‰çš„库存
        /// </summary>
        public async Task<WebResponseContent> BatchReturnStock(string orderNo, string palletCode)
        {
            try
            {
                _unitOfWorkManage.BeginTran();
                //  æŸ¥æ‰¾æ‰˜ç›˜ä¸Šæœªå®Œæˆçš„锁定记录
                var unfinishedLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                    .Where(x => x.OrderNo == orderNo &&
                               x.PalletCode == palletCode &&
                               x.Status == (int)OutLockStockStatusEnum.出库中)
                    .ToListAsync();
                if (!unfinishedLocks.Any())
                    return WebResponseContent.Instance.Error("该托盘没有未完成的锁定记录");
                // æŒ‰æ‰¹æ¬¡åˆ†ç»„处理
                var batchGroups = unfinishedLocks.GroupBy(x => x.BatchNo);
                foreach (var batchGroup in batchGroups)
                {
                    var batchNo = batchGroup.Key;
                    var batchLocks = batchGroup.ToList();
                    // é‡Šæ”¾åº“存和锁定记录
                    foreach (var lockInfo in batchLocks)
                    {
                        await ReleaseLockAndStock(lockInfo);
                    }
                    // æ›´æ–°æ‰¹æ¬¡çŠ¶æ€
                    await UpdateBatchStatusForReturn(batchNo, batchLocks);
                    // æ›´æ–°è®¢å•明细的已分配数量
                    await UpdateOrderDetailAfterReturn(batchLocks);
                }
                _unitOfWorkManage.CommitTran();
                return WebResponseContent.Instance.OK("分批回库成功");
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                _logger.LogError($"分批回库失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error($"分批回库失败:{ex.Message}");
            }
        }
        #endregion
        #region éªŒè¯æ–¹æ³•
        private async Task<ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>> ValidatePickingRequest(
    string orderNo, string palletCode, string barcode)
        {
            // æŸ¥æ‰¾é”å®šä¿¡æ¯
            var lockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(x => x.OrderNo == orderNo &&
                           x.BatchNo == batchNo &&
                           x.PalletCode == palletCode &&
                           x.CurrentBarcode == barcode &&
                           x.Status == (int)OutLockStockStatusEnum.出库中)
                .FirstAsync();
            if (lockInfo == null)
                return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail)>.Error("未找到有效的批次锁定信息");
                return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.Error("未找到有效的锁定信息");
            if (actualPickedQty <= 0 || actualPickedQty > lockInfo.AssignQuantity - lockInfo.PickedQty)
                return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail)>.Error("分拣数量无效");
            // æ£€æŸ¥æ˜¯å¦å·²ç»åˆ†æ‹£å®Œæˆ
            if (lockInfo.PickedQty >= lockInfo.AssignQuantity)
                return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.Error("该条码已分拣完成");
            // èŽ·å–è®¢å•æ˜Žç»†å’Œåº“å­˜æ˜Žç»†
            // èŽ·å–å…³è”æ•°æ®
            var orderDetail = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>()
                .FirstAsync(x => x.Id == lockInfo.OrderDetailId);
            var stockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .FirstAsync(x => x.Barcode == barcode && x.StockId == lockInfo.StockId);
            return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail)>.Success((lockInfo, orderDetail, stockDetail));
            // éªŒè¯åº“存数量
            if (stockDetail.StockQuantity < lockInfo.AssignQuantity)
                return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.Error(
                    $"库存数量不足,需要:{lockInfo.AssignQuantity},实际:{stockDetail.StockQuantity}");
            var batch = await _outboundBatchRepository.Db.Queryable<Dt_OutboundBatch>()
                .FirstAsync(x => x.BatchNo == lockInfo.BatchNo);
            return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.Success((lockInfo, orderDetail, stockDetail, batch));
        }
        private async Task<PickingResult> ExecuteBatchPickingLogic(
        private async Task<ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>> ValidateSplitRequest(
            string orderNo, string palletCode, string originalBarcode, decimal splitQuantity)
        {
            var lockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(x => x.OrderNo == orderNo &&
                           x.PalletCode == palletCode &&
                           x.CurrentBarcode == originalBarcode &&
                           x.Status == (int)OutLockStockStatusEnum.出库中)
                .FirstAsync();
            if (lockInfo == null)
                return ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("未找到有效的锁定信息");
            var stockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .FirstAsync(x => x.Barcode == originalBarcode && x.StockId == lockInfo.StockId);
            if (stockDetail.StockQuantity < splitQuantity)
                return ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("拆包数量不能大于库存数量");
            if (lockInfo.AssignQuantity - lockInfo.PickedQty < splitQuantity)
                return ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("拆包数量不能大于未拣选数量");
            return ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Success((lockInfo, stockDetail));
        }
        private async Task<ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>> ValidateCancelSplitRequest(
            string orderNo, string palletCode, string newBarcode)
        {
            var splitRecord = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
                .Where(x => x.NewBarcode == newBarcode &&
                           x.OrderNo == orderNo &&
                           !x.IsReverted)
                .FirstAsync();
            if (splitRecord == null)
                return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("未找到拆包记录");
            var newLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(x => x.CurrentBarcode == newBarcode &&
                           x.PalletCode == palletCode &&
                           x.OrderNo == orderNo)
                .FirstAsync();
            if (newLockInfo == null)
                return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("未找到新锁定信息");
            // æ£€æŸ¥æ–°æ¡ç æ˜¯å¦å·²è¢«åˆ†æ‹£
            var pickingRecord = await Db.Queryable<Dt_PickingRecord>()
                .Where(x => x.Barcode == newBarcode && !x.IsCancelled)
                .FirstAsync();
            if (pickingRecord != null)
                return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("该条码已被分拣,无法取消拆包");
            var newStockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .FirstAsync(x => x.Barcode == newBarcode);
            return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Success((splitRecord, newLockInfo, newStockDetail));
        }
        #endregion
        #region æ ¸å¿ƒé€»è¾‘方法
        private async Task<PickingResult> ExecutePickingLogic(
            Dt_OutStockLockInfo lockInfo, Dt_OutboundOrderDetail orderDetail,
            Dt_StockInfoDetail stockDetail, decimal actualPickedQty)
        {
@@ -180,86 +435,34 @@
            };
        }
        private async Task UpdateBatchCompletedQuantity(string batchNo, decimal pickedQty)
        private async Task<RevertPickingResult> RevertPickingData(Dt_PickingRecord pickingRecord)
        {
            await _outboundBatchRepository.Db.Updateable<Dt_OutboundBatch>()
                .SetColumns(x => x.CompletedQuantity == x.CompletedQuantity + pickedQty)
                .Where(x => x.BatchNo == batchNo)
                .ExecuteCommandAsync();
            // æ£€æŸ¥æ‰¹æ¬¡æ˜¯å¦å®Œæˆ
            var batch = await _outboundBatchRepository.Db.Queryable<Dt_OutboundBatch>()
                .FirstAsync(x => x.BatchNo == batchNo);
            if (batch.CompletedQuantity >= batch.BatchQuantity)
            {
                batch.BatchStatus = (int)BatchStatusEnum.已完成;
                await _outboundBatchRepository.Db.Updateable(batch).ExecuteCommandAsync();
            }
        }
        #endregion
        #region æ‰‹åŠ¨æ‹†åŒ…
        /// <summary>
        /// æ‰‹åŠ¨æ‹†åŒ…
        /// </summary>
        public async Task<WebResponseContent> ManualSplitPackage(string orderNo, string batchNo, string originalBarcode, decimal splitQuantity)
        {
            try
            {
                _unitOfWorkManage.BeginTran();
                // 1. éªŒè¯æ‹†åŒ…请求
                var validationResult = await ValidateManualSplitRequest(orderNo, batchNo, originalBarcode, splitQuantity);
                if (!validationResult.IsValid)
                    return WebResponseContent.Instance.Error(validationResult.ErrorMessage);
                var (lockInfo, stockDetail) = validationResult.Data;
                // 2. æ‰§è¡Œæ‹†åŒ…逻辑
                var splitResult = await ExecuteManualSplit(lockInfo, stockDetail, splitQuantity, batchNo);
                _unitOfWorkManage.CommitTran();
                return WebResponseContent.Instance.OK("手动拆包成功", new { NewBarcode = splitResult.NewBarcode });
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                _logger.LogError($"手动拆包失败 - OrderNo: {orderNo}, BatchNo: {batchNo}, Barcode: {originalBarcode}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error($"手动拆包失败:{ex.Message}");
            }
        }
        private async Task<ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>> ValidateManualSplitRequest(
            string orderNo, string batchNo, string originalBarcode, decimal splitQuantity)
        {
            // æ¢å¤é”å®šä¿¡æ¯
            var lockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(x => x.OrderNo == orderNo &&
                           x.BatchNo == batchNo &&
                           x.CurrentBarcode == originalBarcode &&
                           x.Status == (int)OutLockStockStatusEnum.出库中)
                .FirstAsync();
                .FirstAsync(x => x.Id == pickingRecord.OutStockLockId);
            if (lockInfo == null)
                return ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("未找到有效的锁定信息");
            lockInfo.PickedQty -= pickingRecord.PickQuantity;
            lockInfo.Status = (int)OutLockStockStatusEnum.出库中;
            await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync();
            // æ¢å¤åº“å­˜
            var stockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .FirstAsync(x => x.Barcode == originalBarcode && x.StockId == lockInfo.StockId);
                .FirstAsync(x => x.Barcode == pickingRecord.Barcode);
            if (stockDetail.StockQuantity < splitQuantity)
                return ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("拆包数量不能大于库存数量");
            stockDetail.StockQuantity += pickingRecord.PickQuantity;
            stockDetail.OutboundQuantity -= pickingRecord.PickQuantity;
            stockDetail.Status = (int)StockStatusEmun.出库锁定;
            await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
            if (lockInfo.AssignQuantity - lockInfo.PickedQty < splitQuantity)
                return ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("拆包数量不能大于未拣选数量");
            return ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Success((lockInfo, stockDetail));
            return new RevertPickingResult
            {
                LockInfo = lockInfo,
                StockDetail = stockDetail
            };
        }
        private async Task<SplitResultDto> ExecuteManualSplit(Dt_OutStockLockInfo lockInfo, Dt_StockInfoDetail stockDetail,
            decimal splitQuantity, string batchNo)
        private async Task<SplitResultDto> ExecuteSplitLogic(Dt_OutStockLockInfo lockInfo, Dt_StockInfoDetail stockDetail,
            decimal splitQuantity, string palletCode)
        {
            // ç”Ÿæˆæ–°æ¡ç 
            string newBarcode = await GenerateNewBarcode();
@@ -289,14 +492,14 @@
            {
                OrderNo = lockInfo.OrderNo,
                OrderDetailId = lockInfo.OrderDetailId,
                BatchNo = batchNo,
                OutboundBatchNo = lockInfo.OutboundBatchNo,
                MaterielCode = lockInfo.MaterielCode,
                StockId = lockInfo.StockId,
                OrderQuantity = splitQuantity,
                AssignQuantity = splitQuantity,
                PickedQty = 0,
                LocationCode = lockInfo.LocationCode,
                PalletCode = lockInfo.PalletCode,
                PalletCode = palletCode,
                Status = (int)OutLockStockStatusEnum.出库中,
                CurrentBarcode = newBarcode,
                Operator = App.User.UserName, 
@@ -305,6 +508,7 @@
            // æ›´æ–°åŽŸé”å®šä¿¡æ¯
            lockInfo.AssignQuantity -= splitQuantity;
            lockInfo.OrderQuantity -= splitQuantity;
            await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync();
            // è®°å½•拆包历史
@@ -313,34 +517,8 @@
            return new SplitResultDto { NewBarcode = newBarcode };
        }
        #endregion
        #region å–消拆包
        /// <summary>
        /// å–消拆包
        /// </summary>
        public async Task<WebResponseContent> CancelSplitPackage(string orderNo, string batchNo, string newBarcode)
        private async Task ExecuteCancelSplitLogic(Dt_SplitPackageRecord splitRecord, Dt_OutStockLockInfo newLockInfo, Dt_StockInfoDetail newStockDetail)
        {
            try
            {
                _unitOfWorkManage.BeginTran();
                // æŸ¥æ‰¾æ‹†åŒ…记录和新锁定信息
                var splitRecord = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
                    .Where(x => x.NewBarcode == newBarcode && x.OrderNo == orderNo && !x.IsReverted)
                    .FirstAsync();
                if (splitRecord == null)
                    return WebResponseContent.Instance.Error("未找到拆包记录");
                var newLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                    .Where(x => x.CurrentBarcode == newBarcode && x.BatchNo == batchNo)
                    .FirstAsync();
                if (newLockInfo == null)
                    return WebResponseContent.Instance.Error("未找到新锁定信息");
                // æ¢å¤åŽŸåº“å­˜
                var originalStock = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                    .FirstAsync(x => x.Barcode == splitRecord.OriginalBarcode && x.StockId == splitRecord.StockId);
@@ -353,11 +531,12 @@
                    .FirstAsync(x => x.Id == splitRecord.OutStockLockInfoId);
                originalLockInfo.AssignQuantity += splitRecord.SplitQty;
            originalLockInfo.OrderQuantity += splitRecord.SplitQty;
                await _outStockLockInfoService.Db.Updateable(originalLockInfo).ExecuteCommandAsync();
                // åˆ é™¤æ–°åº“存明细
                await _stockInfoDetailService.Db.Deleteable<Dt_StockInfoDetail>()
                    .Where(x => x.Barcode == newBarcode)
                .Where(x => x.Barcode == newLockInfo.CurrentBarcode)
                    .ExecuteCommandAsync();
                // åˆ é™¤æ–°é”å®šä¿¡æ¯
@@ -368,66 +547,53 @@
                // æ ‡è®°æ‹†åŒ…记录为已撤销
                splitRecord.IsReverted = true;
                splitRecord.RevertTime = DateTime.Now;
                splitRecord.Operator = App.User.UserName;
            splitRecord.RevertOperator = App.User.UserName;
                await _splitPackageService.Db.Updateable(splitRecord).ExecuteCommandAsync();
                _unitOfWorkManage.CommitTran();
                return WebResponseContent.Instance.OK("取消拆包成功");
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                _logger.LogError($"取消拆包失败 - OrderNo: {orderNo}, BatchNo: {batchNo}, Barcode: {newBarcode}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error($"取消拆包失败:{ex.Message}");
            }
        }
        #endregion
        #region åˆ†æ‰¹å›žåº“
        #region æ•°æ®æ›´æ–°æ–¹æ³•
        /// <summary>
        /// åˆ†æ‰¹å›žåº“ - é‡Šæ”¾æœªæ‹£é€‰çš„库存
        /// </summary>
        public async Task<WebResponseContent> BatchReturnStock(string orderNo, string batchNo)
        private async Task UpdateBatchAndOrderData(Dt_OutboundBatch batch, Dt_OutboundOrderDetail orderDetail, decimal pickedQty, string orderNo)
        {
            try
            // æ›´æ–°æ‰¹æ¬¡å®Œæˆæ•°é‡
            batch.CompletedQuantity += pickedQty;
            if (batch.CompletedQuantity >= batch.BatchQuantity)
            {
                _unitOfWorkManage.BeginTran();
                batch.BatchStatus = (int)BatchStatusEnum.已完成;
            }
            await _outboundBatchRepository.Db.Updateable(batch).ExecuteCommandAsync();
                // 1. æŸ¥æ‰¾æ‰¹æ¬¡æœªå®Œæˆçš„锁定记录
                var unfinishedLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                    .Where(x => x.OrderNo == orderNo &&
                               x.BatchNo == batchNo &&
                               x.Status == (int)OutLockStockStatusEnum.出库中)
                    .ToListAsync();
            // æ›´æ–°è®¢å•明细
            orderDetail.OverOutQuantity += pickedQty;
            orderDetail.AllocatedQuantity -= pickedQty;
            await _outboundOrderDetailService.Db.Updateable(orderDetail).ExecuteCommandAsync();
                if (!unfinishedLocks.Any())
                    return WebResponseContent.Instance.Error("该批次没有未完成的锁定记录");
                // 2. é‡Šæ”¾åº“存和锁定记录
                foreach (var lockInfo in unfinishedLocks)
                {
                    await ReleaseLockAndStock(lockInfo);
            // æ£€æŸ¥è®¢å•状态
            await CheckAndUpdateOrderStatus(orderNo);
                }
                // 3. æ›´æ–°æ‰¹æ¬¡çŠ¶æ€
                await UpdateBatchStatusForReturn(batchNo);
                // 4. æ›´æ–°è®¢å•明细的已分配数量
                await UpdateOrderDetailAfterReturn(unfinishedLocks);
                _unitOfWorkManage.CommitTran();
                return WebResponseContent.Instance.OK("分批回库成功");
            }
            catch (Exception ex)
        private async Task RevertBatchAndOrderData(Dt_PickingRecord pickingRecord)
            {
                _unitOfWorkManage.RollbackTran();
                _logger.LogError($"分批回库失败 - OrderNo: {orderNo}, BatchNo: {batchNo}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error($"分批回库失败:{ex.Message}");
            }
            // æ¢å¤æ‰¹æ¬¡å®Œæˆæ•°é‡
            var batch = await _outboundBatchRepository.Db.Queryable<Dt_OutboundBatch>()
                .FirstAsync(x => x.BatchNo == pickingRecord.BatchNo);
            batch.CompletedQuantity -= pickingRecord.PickQuantity;
            batch.BatchStatus = (int)BatchStatusEnum.执行中;
            await _outboundBatchRepository.Db.Updateable(batch).ExecuteCommandAsync();
            // æ¢å¤è®¢å•明细
            var orderDetail = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>()
                .FirstAsync(x => x.Id == pickingRecord.OrderDetailId);
            orderDetail.OverOutQuantity -= pickingRecord.PickQuantity;
            orderDetail.AllocatedQuantity += pickingRecord.PickQuantity;
            await _outboundOrderDetailService.Db.Updateable(orderDetail).ExecuteCommandAsync();
            // é‡æ–°æ£€æŸ¥è®¢å•状态
            await CheckAndUpdateOrderStatus(pickingRecord.OrderNo);
        }
        private async Task ReleaseLockAndStock(Dt_OutStockLockInfo lockInfo)
@@ -443,20 +609,30 @@
            }
            // æ›´æ–°é”å®šè®°å½•状态为回库
            lockInfo.Status = (int)OutLockStockStatusEnum.回库中;
            lockInfo.Status = (int)OutLockStockStatusEnum.已回库;
            await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync();
        }
        private async Task UpdateBatchStatusForReturn(string batchNo)
        private async Task UpdateBatchStatusForReturn(string batchNo, List<Dt_OutStockLockInfo> returnedLocks)
        {
            await _outboundBatchRepository.Db.Updateable<Dt_OutboundBatch>()
                .SetColumns(x => new Dt_OutboundBatch
            var batch = await _outboundBatchRepository.Db.Queryable<Dt_OutboundBatch>()
                .FirstAsync(x => x.BatchNo == batchNo);
            // è®¡ç®—回库数量
            var returnedQty = returnedLocks.Sum(x => x.AssignQuantity - x.PickedQty);
            batch.CompletedQuantity -= returnedQty;
            if (batch.CompletedQuantity <= 0)
                {
                    BatchStatus = (int)BatchStatusEnum.已回库,
                    Operator = App.User.UserName
                })
                .Where(x => x.BatchNo == batchNo)
                .ExecuteCommandAsync();
                batch.BatchStatus = (int)BatchStatusEnum.已回库;
            }
            else
            {
                batch.BatchStatus = (int)BatchStatusEnum.执行中;
            }
            batch.Operator = App.User.UserName;
            await _outboundBatchRepository.Db.Updateable(batch).ExecuteCommandAsync();
        }
        private async Task UpdateOrderDetailAfterReturn(List<Dt_OutStockLockInfo> returnedLocks)
@@ -482,7 +658,7 @@
        private async Task<string> GenerateNewBarcode()
        {
            var seq = await _dailySequenceService.GetNextSequenceAsync();
            return "WSLOT" + DateTime.Now.ToString("yyyyMMdd") + seq.ToString()?.PadLeft(5, '0');
            return "WSLOT" + DateTime.Now.ToString("yyyyMMdd") + seq.ToString().PadLeft(5, '0');
        }
        private async Task RecordSplitHistory(Dt_OutStockLockInfo lockInfo, Dt_StockInfoDetail stockDetail,decimal splitQty, string newBarcode)
@@ -503,12 +679,12 @@
            await _splitPackageService.Db.Insertable(splitHistory).ExecuteCommandAsync();
        }
        private async Task RecordPickingHistory(PickingResult result, string orderNo, string palletCode, string batchNo)
        private async Task RecordPickingHistory(PickingResult result, string orderNo, string palletCode)
        {
            var pickingRecord = new Dt_PickingRecord
            {
                OrderNo = orderNo,
               // BatchNo = batchNo,
                // BatchNo = result.FinalLockInfo.BatchNo,
                OrderDetailId = result.FinalLockInfo.OrderDetailId,
                PalletCode = palletCode,
                Barcode = result.FinalLockInfo.CurrentBarcode,
@@ -516,47 +692,60 @@
                PickQuantity = result.ActualPickedQty,
                PickTime = DateTime.Now,
                Operator = App.User.UserName,
                OutStockLockId = result.FinalLockInfo.Id
                OutStockLockId = result.FinalLockInfo.Id,
                //  IsCancelled = false
            };
            await Db.Insertable(pickingRecord).ExecuteCommandAsync();
        }
        private async Task UpdateOrderRelatedData(int orderDetailId, decimal pickedQty, string orderNo)
        {
            // æ›´æ–°è®¢å•明细的已出库数量
            await _outboundOrderDetailService.Db.Updateable<Dt_OutboundOrderDetail>()
                .SetColumns(x => new Dt_OutboundOrderDetail
                {
                    OverOutQuantity = x.OverOutQuantity + pickedQty,
                    AllocatedQuantity = x.AllocatedQuantity - pickedQty
                })
                .Where(x => x.Id == orderDetailId)
                .ExecuteCommandAsync();
            // æ£€æŸ¥è®¢å•状态
            await CheckAndUpdateOrderStatus(orderNo);
        }
        private async Task CheckAndUpdateOrderStatus(string orderNo)
        {
            var orderDetails = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>()
                  .LeftJoin<Dt_OutboundOrder>((o, item) => o.OrderId == item.Id)
                  .Where((o, item) => item.OrderNo == orderNo)
                  .Select((o, item) => o)
                .LeftJoin<Dt_OutboundOrder>((detail, order) => detail.OrderId == order.Id)
                .Where((detail, order) => order.OrderNo == orderNo)
                .Select((detail, order) => detail)
                  .ToListAsync();
            bool allCompleted = orderDetails.All(x => x.OverOutQuantity >= x.NeedOutQuantity);
            if (allCompleted)
            {
            var orderStatus = allCompleted ? (int)OutOrderStatusEnum.出库完成 : (int)OutOrderStatusEnum.出库中;
                await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
                    .SetColumns(x => new Dt_OutboundOrder { OrderStatus = (int)OutOrderStatusEnum.出库完成 })
                .SetColumns(x => x.OrderStatus == orderStatus)
                    .Where(x => x.OrderNo == orderNo)
                    .ExecuteCommandAsync();
            }
        #endregion
        #region DTOç±»
        public class PickingResult
        {
            public Dt_OutStockLockInfo FinalLockInfo { get; set; }
            public decimal ActualPickedQty { get; set; }
        }
        public class RevertPickingResult
        {
            public Dt_OutStockLockInfo LockInfo { get; set; }
            public Dt_StockInfoDetail StockDetail { get; set; }
        }
        public class SplitResultDto
        {
            public string NewBarcode { get; set; }
        }
        public class ValidationResult<T>
        {
            public bool IsValid { get; set; }
            public string ErrorMessage { get; set; }
            public T Data { get; set; }
            public static ValidationResult<T> Success(T data) => new ValidationResult<T> { IsValid = true, Data = data };
            public static ValidationResult<T> Error(string message) => new ValidationResult<T> { IsValid = false, ErrorMessage = message };
        }
        #endregion
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundOrderDetailService.cs
@@ -66,6 +66,11 @@
            var outboundOrder = _outboundOrderService.Db.Queryable<Dt_OutboundOrder>()
                .First(x => x.Id == orderId);
            if (!CanReassignOrder(outboundOrder))
            {
                throw new Exception($"订单{outboundOrder.OrderNo}状态不允许重新分配库存");
            }
            List<Dt_StockInfo> outStocks = new List<Dt_StockInfo>();
            List<Dt_OutStockLockInfo> outStockLockInfos = new List<Dt_OutStockLockInfo>();
            List<Dt_LocationInfo> locationInfos = new List<Dt_LocationInfo>();
@@ -79,7 +84,7 @@
                    BatchNo = x.Key.BatchNo,
                    SupplyCode = x.Key.SupplyCode,
                    Details = x.ToList(),
                    TotalNeedQuantity = x.Sum(v => v.OrderQuantity - v.OverOutQuantity - v.LockQuantity - v.MoveQty)
                    TotalNeedQuantity = CalculateReassignNeedQuantity(x.ToList())
                })
                .Where(x => x.TotalNeedQuantity > 0)
                .ToList();
@@ -128,6 +133,39 @@
            return (outStocks, outboundOrderDetails, outStockLockInfos, locationInfos);
        }
        /// <summary>
        /// æ£€æŸ¥è®¢å•是否允许重新分配
        /// </summary>
        private bool CanReassignOrder(Dt_OutboundOrder outboundOrder)
        {
            // å…è®¸é‡æ–°åˆ†é…çš„状态
            var allowedStatus = new[] {OutOrderStatusEnum.未开始, OutOrderStatusEnum.出库中,OutOrderStatusEnum.部分完成};
            return allowedStatus.Contains((OutOrderStatusEnum)outboundOrder.OrderStatus);
        }
        /// <summary>
        /// é‡æ–°è®¡ç®—需求数量(考虑重新分配的场景)
        /// </summary>
        private decimal CalculateReassignNeedQuantity(List<Dt_OutboundOrderDetail> details)
        {
            decimal totalNeed = 0;
            foreach (var detail in details)
            {
                // å…³é”®ä¿®æ”¹ï¼šé‡æ–°åˆ†é…æ—¶ï¼Œåªè€ƒè™‘未出库的部分,忽略锁定数量
                // å› ä¸ºé”å®šæ•°é‡åœ¨å›žåº“时已经被重置
                decimal remainingNeed = detail.OrderQuantity - detail.OverOutQuantity - detail.MoveQty;
                if (remainingNeed > 0)
                {
                    totalNeed += remainingNeed;
                }
            }
            return totalNeed;
        }
        private (bool success, string message) DistributeLockQuantityByFIFO(
           List<Dt_OutboundOrderDetail> details,
           List<Dt_StockInfo> assignStocks,
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundOrderService.cs
@@ -72,7 +72,7 @@
                    item.MoveQty = moveissueoStockResult.Quantity;
                }
                if (model.OrderType != InOrderTypeEnum.Allocat.ObjToInt() || model.OrderType != InOrderTypeEnum.InternalAllocat.ObjToInt())
                if (model.OrderType != InOrderTypeEnum.AllocatOutbound.ObjToInt() || model.OrderType != InOrderTypeEnum.InternalAllocat.ObjToInt())
                {
                    model.OrderNo = CreateCodeByRule(nameof(RuleCodeEnum.OutboundOrderRule));
                }
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundPickingService.cs
@@ -295,6 +295,8 @@
                //执行回库操作
                await ExecuteReturnOperations(orderNo, palletCode, stockInfo, task, statusAnalysis);
                await ReleaseAllLocksForReallocation(orderNo, palletCode, statusAnalysis);
                _unitOfWorkManage.CommitTran();
@@ -1307,6 +1309,116 @@
            await UpdateStockInfoStatus(stockInfo);
        }
        /// <summary>
        /// å®Œå…¨é‡Šæ”¾é”å®šï¼Œå…è®¸é‡æ–°åˆ†é…åº“å­˜
        /// </summary>
        private async Task ReleaseAllLocksForReallocation(string orderNo, string palletCode, PalletStatusAnalysis analysis)
        {
            _logger.LogInformation($"开始释放锁定以便重新分配 - è®¢å•: {orderNo}, æ‰˜ç›˜: {palletCode}");
            // 1. å¤„理未分拣的出库锁定记录 - å®Œå…¨é‡Šæ”¾
            if (analysis.HasRemainingLocks)
            {
                await ReleaseRemainingLocks(analysis.RemainingLocks);
            }
            // 2. å¤„理已回库的锁定记录 - åˆ é™¤æˆ–标记为无效
            await CleanupReturnedLocks(orderNo, palletCode);
            // 3. é‡ç½®è®¢å•明细的锁定数量
            await ResetOrderDetailLockQuantities(analysis);
            _logger.LogInformation($"锁定释放完成 - è®¢å•: {orderNo}, æ‰˜ç›˜: {palletCode}");
        }
        /// <summary>
        /// é‡Šæ”¾æœªåˆ†æ‹£çš„锁定记录
        /// </summary>
        private async Task ReleaseRemainingLocks(List<Dt_OutStockLockInfo> remainingLocks)
        {
            var lockIds = remainingLocks.Select(x => x.Id).ToList();
            // å°†é”å®šè®°å½•状态改为"已释放",或者直接删除
            //  æ ‡è®°ä¸ºå·²é‡Šæ”¾
            await _outStockLockInfoService.Db.Updateable<Dt_OutStockLockInfo>()
                .SetColumns(it => new Dt_OutStockLockInfo
                {
                    Status = (int)OutLockStockStatusEnum.已释放, // éœ€è¦æ–°å¢žè¿™ä¸ªçŠ¶æ€
                   // ReleaseTime = DateTime.Now,
                    Operator = App.User.UserName
                })
                .Where(it => lockIds.Contains(it.Id))
                .ExecuteCommandAsync();
            //  ç›´æŽ¥åˆ é™¤ï¼ˆæ›´å½»åº•)
            // await _outStockLockInfoService.Db.Deleteable<Dt_OutStockLockInfo>()
            //     .Where(it => lockIds.Contains(it.Id))
            //     .ExecuteCommandAsync();
            _logger.LogInformation($"释放{remainingLocks.Count}条未分拣锁定记录");
        }
        /// <summary>
        /// æ¸…理已回库的锁定记录
        /// </summary>
        private async Task CleanupReturnedLocks(string orderNo, string palletCode)
        {
            // æŸ¥æ‰¾æ‰€æœ‰çŠ¶æ€ä¸ºå›žåº“ä¸­çš„é”å®šè®°å½•å¹¶é‡Šæ”¾
            var returnedLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(it => it.OrderNo == orderNo &&
                           it.PalletCode == palletCode &&
                           it.Status == (int)OutLockStockStatusEnum.回库中)
                .ToListAsync();
            if (returnedLocks.Any())
            {
                var returnedLockIds = returnedLocks.Select(x => x.Id).ToList();
                await _outStockLockInfoService.Db.Updateable<Dt_OutStockLockInfo>()
                    .SetColumns(it => new Dt_OutStockLockInfo
                    {
                        Status = (int)OutLockStockStatusEnum.已释放,
                        //ReleaseTime = DateTime.Now,
                        Operator = App.User.UserName
                    })
                    .Where(it => returnedLockIds.Contains(it.Id))
                    .ExecuteCommandAsync();
                _logger.LogInformation($"清理{returnedLocks.Count}条回库中锁定记录");
            }
        }
        /// <summary>
        /// é‡ç½®è®¢å•明细的锁定数量
        /// </summary>
        private async Task ResetOrderDetailLockQuantities(PalletStatusAnalysis analysis)
        {
            // æ”¶é›†æ‰€æœ‰å—影响的订单明细ID
            var affectedOrderDetailIds = new HashSet<int>();
            if (analysis.HasRemainingLocks)
            {
                foreach (var lockInfo in analysis.RemainingLocks)
                {
                    affectedOrderDetailIds.Add(lockInfo.OrderDetailId);
                }
            }
            // é‡ç½®è¿™äº›è®¢å•明细的锁定数量
            foreach (var orderDetailId in affectedOrderDetailIds)
            {
                await _outboundOrderDetailService.Db.Updateable<Dt_OutboundOrderDetail>()
                    .SetColumns(it => new Dt_OutboundOrderDetail
                    {
                        LockQuantity = 0, // é‡ç½®é”å®šæ•°é‡
                        OrderDetailStatus = OrderDetailStatusEnum.New.ObjToInt() // é‡ç½®çŠ¶æ€ä¸ºæ–°å»º
                    })
                    .Where(it => it.Id == orderDetailId)
                    .ExecuteCommandAsync();
            }
            _logger.LogInformation($"重置{affectedOrderDetailIds.Count}个订单明细的锁定数量");
        }
        private async Task HandleRemainingLocksReturn(List<Dt_OutStockLockInfo> remainingLocks, int stockId)
        {
            var lockIds = remainingLocks.Select(x => x.Id).ToList();
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs
@@ -384,6 +384,13 @@
            return WebResponseContent.Instance.OK();
        }
        public async Task<WebResponseContent> OutAllocateTaskCompleted(Dt_Task task)
        {
            _logger.LogInformation($"TaskService  OutAllocateTaskCompleted: {task.TaskNum}");
           return  await OutboundTaskCompleted(task);
        }
        public async Task<WebResponseContent> OutboundTaskCompleted(Dt_Task task)
        {
            _logger.LogInformation($"TaskService  OutboundTaskCompleted: {task.TaskNum}");
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_Outbound.cs
@@ -143,14 +143,22 @@
            {
                throw new Exception("未找到出库单明细信息");
            }
            if (outboundOrderDetails.FirstOrDefault(x => x.OrderDetailStatus > OrderDetailStatusEnum.New.ObjToInt() && x.OrderDetailStatus != OrderDetailStatusEnum.AssignOverPartial.ObjToInt()) != null)
            //if (outboundOrderDetails.FirstOrDefault(x => x.OrderDetailStatus > OrderDetailStatusEnum.New.ObjToInt() && x.OrderDetailStatus != OrderDetailStatusEnum.AssignOverPartial.ObjToInt()) != null)
            //{
            //    throw new Exception("所选出库单明细存在出库中或已完成");
            //}
            if (outboundOrderDetails.FirstOrDefault(x => x.OrderDetailStatus > OrderDetailStatusEnum.Outbound.ObjToInt() && x.OrderDetailStatus != OrderDetailStatusEnum.AssignOverPartial.ObjToInt()) != null)
            {
                throw new Exception("所选出库单明细存在出库中或已完成");
                throw new Exception("所选出库单明细存在已完成状态,无法重新分配");
            }
            List<Dt_StockInfo>? stockInfos = null;
            List<Dt_OutboundOrderDetail>? orderDetails = null;
            List<Dt_OutStockLockInfo>? outStockLockInfos = null;
            List<Dt_LocationInfo>? locationInfos = null;
            CleanupPreviousInvalidLocks(outboundOrderDetails);
            (List<Dt_StockInfo>, List<Dt_OutboundOrderDetail>, List<Dt_OutStockLockInfo>, List<Dt_LocationInfo>) result = _outboundOrderDetailService.AssignStockOutbound(outboundOrderDetails);
            if (result.Item1 != null && result.Item1.Count > 0)
@@ -188,7 +196,31 @@
            }
            return (tasks, stockInfos, orderDetails, outStockLockInfos, locationInfos);
        }
        /// <summary>
        /// æ¸…理之前的无效锁定记录
        /// </summary>
        private  void  CleanupPreviousInvalidLocks(List<Dt_OutboundOrderDetail> orderDetails)
        {
            var orderIds = orderDetails.Select(x => x.OrderId).Distinct().ToList();
            var orderNos =  _outboundOrderService.Db.Queryable<Dt_OutboundOrder>()
                .Where(x => orderIds.Contains(x.Id))
                .Select(x => x.OrderNo)
                .ToList();
            // æ¸…理状态为"已释放"或"回库中"的旧锁定记录
            foreach (var orderNo in orderNos)
            {
               _outStockLockInfoService.Db.Updateable<Dt_OutStockLockInfo>()
                    .SetColumns(x => new Dt_OutStockLockInfo
                    {
                        Status = (int)OutLockStockStatusEnum.已释放
                    })
                    .Where(x => x.OrderNo == orderNo &&
                               (x.Status == (int)OutLockStockStatusEnum.回库中 ||
                                x.Status == (int)OutLockStockStatusEnum.已释放))
                    .ExecuteCommand();
            }
        }
        /// <summary>
        /// ç”Ÿæˆå‡ºåº“任务后数据更新到数据库
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Outbound/OutboundBatchPickingController.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Mvc;
using WIDESEA_Core.BaseController;
using WIDESEA_IOutboundService;
using WIDESEA_Model.Models;
namespace WIDESEA_WMSServer.Controllers.Outbound
{
    [Route("api/OutboundBatchPicking")]
    [ApiController]
    public class OutboundBatchPickingController : ApiBaseController<IOutboundBatchPickingService, Dt_PickingRecord>
    {
        private readonly ISplitPackageService _splitPackageService;
        private readonly IOutStockLockInfoService _outStockLockInfoService;
        public OutboundBatchPickingController(IOutboundBatchPickingService service, ISplitPackageService splitPackageService, IOutStockLockInfoService outStockLockInfoService) : base(service)
        {
            _splitPackageService = splitPackageService;
            _outStockLockInfoService = outStockLockInfoService;
        }
    }
}