1
647556386
2025-11-30 8639f19c82f6e263654db44286256bb8d028d2c2
1
已添加12个文件
已修改67个文件
10288 ■■■■ 文件已修改
项目代码/WIDESEA_WMSClient/src/extension/check/extend/StockSelect.vue 347 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/extension/check/recheckOrder.js 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/extension/inbound/allocateinboundOrder.js 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/extension/inbound/extend/Pallet.vue 1985 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/extension/inbound/extend/SelectedStock.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/extension/inbound/extend/StockSelect.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/extension/inbound/extend/allocateOrderDetail.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/extension/inbound/inboundOrder.js 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/extension/outbound/extend/SelectedStock.vue 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/extension/outbound/extend/outOrderDetail.vue 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/extension/outbound/extend/printView.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/extension/outbound/outboundOrder.js 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/router/viewGird.js 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/check/ReCheckOrder.vue 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/check/checkOrder.vue 176 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/inbound/Dt_AllocateOrder.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/inbound/allocateinboundOrder.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/inbound/inboundOrder.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/outbound/BatchPickingConfirm.vue 1278 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/outbound/PickingConfirm.vue 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/outbound/allocateoutboundOrder.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/outbound/outboundOrder.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/record/locationStatusChangeRecord.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/record/stockQuantityChangeRecord.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/stock/stockInfo.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | 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_AllocateService/AllocateService.cs 136 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_BasicService/DailySequenceService.cs 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_BasicService/InvokeMESService.cs 304 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_BasicService/LocationInfoService.cs 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_BasicService/MaterialUnitService.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_BasicService/WIDESEA_BasicService.csproj 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Common/CommonEnum/PalletTypeEnum.cs 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Common/OrderEnum/InboundOrderMenu.cs 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Common/OrderEnum/OutboundOrderEnum.cs 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Common/StockEnum/OutLockStockStatusEnum.cs 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Common/StockEnum/StockStatusEmun.cs 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Core/Helper/SqlSugarHelper.cs 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_DTO/Allocate/AllocateDto.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_DTO/Outbound/BatchOutBoundDto.cs 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_DTO/Outbound/OutboundOrderAddDTO.cs 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_DTO/Outbound/OutboundOrderGetDTO.cs 45 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_DTO/Stock/StockSelectViewDTO.cs 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_DTO/Task/WMSTaskDTO.cs 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_IBasicService/IMaterialUnitService.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_IOutboundService/IOutStockLockInfoService.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_IOutboundService/IOutboundBatchPickingService.cs 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_IOutboundService/IOutboundOrderDetailService.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_IStockService/IStockInfoService.cs 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_ITaskInfoService/ITaskService.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_InboundService/InboundOrderService.cs 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Model/Models/Allocate/Dt_AllocateOrderDetail.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Model/Models/Outbound/Dt_OutboundBatch.cs 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Model/Models/Outbound/Dt_OutboundLockInfo.cs 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Model/Models/Outbound/Dt_OutboundOrder.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Model/Models/Outbound/Dt_OutboundOrderDetail.cs 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Model/Models/Outbound/Dt_PickingRecord.cs 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_OutboundService/OutStockLockInfoService.cs 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundBatchPickingService.cs 1441 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundOrderDetailService.cs 632 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundOrderService.cs 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundPickingService.cs 1343 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_OutboundService/WIDESEA_OutboundService.csproj 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs 149 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs 371 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_Inbound.cs 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_Outbound.cs 260 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WIDESEA_TaskInfoService.csproj 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Allocate/AllocateOrderController.cs 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Check/ReCheckOrderController.cs 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/ESSController.cs 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Inbound/InboundOrderController.cs 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Outbound/OutboundBatchPickingController.cs 219 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Outbound/OutboundPickingController.cs 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Stock/StockInfoController.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/TaskInfo/TaskController.cs 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_WMSServer/Jobs/InventoryLockJob.cs 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/extension/check/extend/StockSelect.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,347 @@
<template>
  <div>
    <vol-box
      v-model="showDetialBox"
      :lazy="true"
      width="60%"
      :padding="15"
      title="指定库存"
    >
      <div class="box-head">
        <el-alert :closable="false" style="width: 100%">
          <el-row>
            <el-col :span="16">
              <span class="less-style">物料名称: {{ row.materielName }} </span>
              <el-divider direction="vertical"></el-divider>
              <span class="less-style">物料编号: {{ row.materielCode }} </span>
              <el-divider direction="vertical"></el-divider>
              <span class="less-style">需求数量: {{ row.qty }} </span>
              <el-divider direction="vertical"></el-divider>
              <span :class="selectionClass">已选数量: {{ selectionSum }} </span>
            </el-col>
            <el-col :span="8">
              <el-link
                type="primary"
                size="small"
                style="float: right; height: 20px; margin-right: 10px"
                @click="getData"
                >刷新</el-link
              >
              <el-link
                type="primary"
                size="small"
                style="float: right; height: 20px; margin-right: 10px"
                @click="openOutboundDialog"
                >直接出库</el-link
              >
            </el-col>
          </el-row>
        </el-alert>
      </div>
      <div class="box-table" style="margin-top: 1%">
        <el-table
          ref="singleTable"
          :data="tableData"
          style="width: 100%; height: 100%"
          highlight-current-row
          @row-click="handleRowClick"
          height="500px"
          @selection-change="handleSelectionChange"
        >
          <el-table-column type="selection" width="55"> </el-table-column>
          <el-table-column
            label="序号"
            type="index"
            fixed="left"
            width="55"
            align="center"
          ></el-table-column>
          <el-table-column
            v-for="(item, index) in tableColumns.filter((x) => !x.hidden)"
            :key="index"
            :prop="item.prop"
            :label="item.title"
            :width="item.width"
            align="center"
          >
            <template #default="scoped" v-if="item.type == 'icon'">
              <el-tooltip
                class="item"
                effect="dark"
                :content="item.title"
                placement="bottom"
                ><el-button
                  type="text"
                  @click="tableButtonClick(scoped.row, item)"
                  ><i :class="item.icon" style="font-size: 22px"></i></el-button
              ></el-tooltip>
            </template>
          </el-table-column>
        </el-table>
      </div>
      <template #footer>
        <el-button type="danger" size="small" @click="showDetialBox = false"
          >关闭</el-button
        >
      </template>
    </vol-box>
    <!-- å‡ºåº“站台选择弹窗(静态模板实现) -->
    <el-dialog
      v-model="showOutboundDialog"
      title="出库操作 - é€‰æ‹©å‡ºåº“站台"
      width="500px"
      :append-to-body="true"
    >
      <el-form
        :model="outboundForm"
        :rules="outboundRules"
        ref="outboundFormRef"
        label-width="100px"
        style="padding: 0 20px"
      >
        <el-form-item label="出库站台" prop="selectedPlatform" style="margin-bottom: 24px">
          <el-select
            v-model="outboundForm.selectedPlatform"
            placeholder="请选择出库站台(3-12)"
            style="width: 100%; height: 40px"
          >
            <el-option
              v-for="platform in platformOptions"
              :key="platform.value"
              :label="platform.label"
              :value="platform.value"
            ></el-option>
          </el-select>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="showOutboundDialog = false" style="margin-right: 8px">取消</el-button>
        <el-button type="primary" @click="confirmOutbound">确定出库</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script>
import VolBox from "@/components/basic/VolBox.vue";
import { ElMessage } from "element-plus";
export default {
  components: { VolBox },
  data() {
    return {
      row: null,
      showDetialBox: false,
      tableData: [],
      tableColumns: [
        { prop: "materielCode", title: "物料编号", type: "string", width: 150 },
        { prop: "barcode", title: "物料条码", type: "string", width: 150 },
        { prop: "palletCode", title: "托盘编号", type: "string", width: 150 },
        { prop: "locationCode", title: "货位编号", type: "string", width: 180 },
        { prop: "useableQuantity", title: "可用数量", type: "string" },
      ],
      selection: [],
      selectionSum: 0,
      selectionClass: "less-style",
      originalQuantity: 0,
      // å‡ºåº“弹窗相关数据
      showOutboundDialog: false,
      outboundForm: { selectedPlatform: "" }, // è¡¨å•绑定数据
      outboundRules: {
        selectedPlatform: [
          { required: true, message: "请选择出库站台", trigger: "change" },
        ],
      },
      platformOptions: [
        { label: "站台2", value: "2-1" },
        { label: "站台3", value: "3-1" },
      ],
    };
  },
  methods: {
    open(row) {
      this.row = row;
      this.showDetialBox = true;
      this.getData();
      console.log(row.id)
      this.updateSelectionClass(); // åˆå§‹åŒ–已选数量样式
    },
    lockStock() {
      this.http
        .post(
          "api/OutboundOrderDetail/LockOutboundStock?id=" + this.row.id,
          this.selection,
          "数据处理中"
        )
        .then((x) => {
          if (!x.status) return ElMessage.error(x.message);
          ElMessage.success("操作成功");
          this.showDetialBox = false;
          this.$emit("parentCall", ($vue) => {
            $vue.getData();
          });
        });
    },
    // æ‰“开出库弹窗
    openOutboundDialog() {
      if (this.selection.length === 0) {
        return ElMessage.error("请选择单据明细");
      }
      // é‡ç½®è¡¨å•避免残留值
      this.outboundForm.selectedPlatform = "";
      this.showOutboundDialog = true;
    },
    // ç¡®è®¤å‡ºåº“操作
    confirmOutbound() {
      this.$refs.outboundFormRef.validate((valid) => {
        if (!valid) return;
     if (this.selection.length <= 0) {
        return this.$message.error("请勾选");
      }
      let url = this.pkcx
        ? "api/Task/GenerateOutboundTask?orderDetailId="
        : "api/Task/GenerateOutboundTask?orderDetailId=";
      this.http
        .post(url + this.row.id, this.selection, "数据处理中")
        .then((x) => {
          if (!x.status) return this.$message.error(x.message);
          this.$message.success("操作成功");
          this.showDetialBox = false;
          this.$emit("parentCall", ($vue) => {
            $vue.getData();
          });
        });
      });
    },
    // å›ºå®šæŸ¥è¯¢ç«‹åº“库存
    getData() {
      const url = "api/StockInfo/GetStockSelectViews?materielCode=";
      this.http
        .post(
          url + this.row.materielCode + "&orderId=" + this.row.id,
          null,
          "查询中"
        )
        .then((x) => {
          this.tableData = x;
          // åˆ·æ–°åŽæ¸…空之前的选择和计数
          this.clearSelection();
          this.selectionSum = 0;
          this.originalQuantity = 0;
          this.updateSelectionClass();
        });
    },
    revokeAssign() {
      this.http
        .post(
          "api/OutboundOrderDetail/RevokeLockOutboundStock?id=" + this.row.id,
          null,
          "数据处理中"
        )
        .then((x) => {
          if (!x.status) return ElMessage.error(x.message);
          ElMessage.success("操作成功");
          this.showDetialBox = false;
          this.$emit("parentCall", ($vue) => {
            $vue.getData();
          });
        });
    },
    handleSelectionChange(val) {
      this.selection = val;
      // è®¡ç®—已选数量(转数字避免字符串拼接)
      this.selectionSum = val.reduce(
        (acc, curr) => acc + Number(curr.useableQuantity || 0),
        0
      ) + this.originalQuantity;
      this.updateSelectionClass();
    },
    // æ›´æ–°å·²é€‰æ•°é‡æ ·å¼
    updateSelectionClass() {
      if (!this.row) return;
      if (this.selectionSum === this.row.orderQuantity) {
        this.selectionClass = "equle-style";
      } else if (this.selectionSum < this.row.orderQuantity) {
        this.selectionClass = "less-style";
      } else {
        this.selectionClass = "more-style";
      }
    },
    toggleSelection(rows) {
      rows ? rows.forEach((row) => this.$refs.singleTable.toggleRowSelection(row)) : this.clearSelection();
    },
    clearSelection() {
      if (this.$refs.singleTable) {
        this.$refs.singleTable.clearSelection();
      }
    },
    handleRowClick(row) {
      this.$refs.singleTable.toggleRowSelection(row);
    },
    // å›¾æ ‡æŒ‰é’®ç‚¹å‡»å ä½æ–¹æ³•(可根据需求扩展)
    tableButtonClick(row, item) {
      console.log("图标按钮点击:", item.title, row);
    },
  },
};
</script>
<style scoped>
.less-style {
  color: black;
}
.equle-style {
  color: green;
}
.more-style {
  color: red;
}
</style>
<style>
.text-button:hover {
  background-color: #f0f9eb !important;
}
.el-table .warning-row {
  background: oldlace;
}
.box-table .el-table tbody tr:hover > td {
  background-color: #d8e0d4 !important;
}
.box-table .el-table tbody tr.current-row > td {
  background-color: #f0f9eb !important;
}
.el-table .success-row {
  background: #f0f9eb;
}
.box-table .el-table {
  border: 1px solid #ebeef5;
}
.box-head .el-alert__content {
  width: 100%;
}
</style>
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/extension/check/recheckOrder.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
/*****************************************************************************************
**  Author:jxx 2022
**  QQ:283591387
**完整文档见:http://v2.volcore.xyz/document/api ã€ä»£ç ç”Ÿæˆé¡µé¢ViewGrid】
**常用示例见:http://v2.volcore.xyz/document/vueDev
**后台操作见:http://v2.volcore.xyz/document/netCoreDev
*****************************************************************************************/
//此js文件是用来自定义扩展业务代码,可以扩展一些自定义页面或者重新配置生成的代码
import gridBody from './extend/StockSelect.vue'
let extension = {
  components: {
    //查询界面扩展组件
    gridHeader: '',
    gridBody: gridBody,
    gridFooter: '',
    //新建、编辑弹出框扩展组件
    modelHeader: '',
    modelBody: '',
    modelFooter: ''
  },
  tableAction: '', //指定某张表的权限(这里填写表名,默认不用填写)
  buttons: { view: [], box: [], detail: [] }, //扩展的按钮
  methods: {
     //下面这些方法可以保留也可以删除
    onInit() {  //框架初始化配置前,
        this.columns.push({
        field: '操作',
        title: '操作',
        width: 90,
        fixed: 'right',
        align: 'center',
        formatter: (row) => {
          return (
            '<i style="cursor: pointer;color: #2d8cf0;"class="el-icon-view">指定库存</i>'
          );
        },
        click: (row) => {
          const table = this.$refs.table.$refs.table;
          if (table) {
            table.clearSelection();
            table.toggleRowSelection(row, true);
          }
          this.$refs.gridBody.open(row);
        }
      });
    },
    onInited() {
      //框架初始化配置后
      //如果要配置明细表,在此方法操作
      //this.detailOptions.columns.forEach(column=>{ });
    },
    searchBefore(param) {
      //界面查询前,可以给param.wheres添加查询参数
      //返回false,则不会执行查询
      return true;
    },
    searchAfter(result) {
      //查询后,result返回的查询数据,可以在显示到表格前处理表格的值
      return true;
    },
    addBefore(formData) {
      //新建保存前formData为对象,包括明细表,可以给给表单设置值,自己输出看formData的值
      return true;
    },
    updateBefore(formData) {
      //编辑保存前formData为对象,包括明细表、删除行的Id
      return true;
    },
    rowClick({ row, column, event }) {
      //查询界面点击行事件
      // this.$refs.table.$refs.table.toggleRowSelection(row); //单击行时选中当前行;
    },
    modelOpenAfter(row) {
      //点击编辑、新建按钮弹出框后,可以在此处写逻辑,如,从后台获取数据
      //(1)判断是编辑还是新建操作: this.currentAction=='Add';
      //(2)给弹出框设置默认值
      //(3)this.editFormFields.字段='xxx';
      //如果需要给下拉框设置默认值,请遍历this.editFormOptions找到字段配置对应data属性的key值
      //看不懂就把输出看:console.log(this.editFormOptions)
    }
  }
};
export default extension;
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/extension/inbound/allocateinboundOrder.js
@@ -40,7 +40,7 @@
          const targetRow = selectedRows[0];
     
          this.$emit('openPalletDialog', targetRow.inboundOrderNo);
          this.$emit('openPalletDialog', targetRow.orderNo);
        }
      },
      {
@@ -71,9 +71,8 @@
                        // å‘起撤销组盘请求
                        try {
                            //console.log('发起撤销组盘请求,托盘号:', formData.palletCode.trim());
                            const response = await http.post('/api/InboundOrder/CancelPalletGroup', {
                                palletCode: formData.palletCode.trim()
                            });
                            const response = await http.post('/api/InboundOrder/UndoPalletGroup?palletCode='+formData.palletCode.trim());
                            const { status, message, data } = response;
                            if (status) {
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/extension/inbound/extend/Pallet.vue
@@ -1,37 +1,16 @@
<template>
   <vol-box
    v-model="groupPalletVisible"
    :title="'组盘操作 - å•据号:' + currentDocNo"
    :height="1000"
    :width="1100"
    :padding="20"
    :modal="true"
     @open="handleDialogOpen"
    @close="handleDialogClose"
  >
  <div class="barcode-scanner-container">
  <vol-box v-model="groupPalletVisible" :title="'组盘操作 - å•据号:' + currentDocNo" :height="1000" :width="1100" :padding="20"
    :modal="true" @open="handleDialogOpen" @close="handleDialogClose">
    <div class="barcode-scanner-container">
      <!-- ä»“库选择 - ç´§å‡‘布局 -->
      <div class="location-section compact">
        <el-form :model="form" :rules="rules" ref="locationForm" class="compact-form">
          <el-form-item label="仓库" prop="warehouseType" class="location-select compact-item">
            <el-select
              v-model="form.warehouseType"
              placeholder="请选择仓库"
              clearable
              filterable
              @change="handleWarehouseChange"
              style="width: 100%"
              :loading="warehouseLoading"
              size="medium"
            >
              <el-option
                v-for="item in warehouseTypes"
                :key="item.warehouseType"
                :label="item.warehouseTypeDesc"
                :value="item.warehouseType"
              />
            <el-select v-model="form.warehouseType" placeholder="请选择仓库" clearable filterable
              @change="handleWarehouseChange" style="width: 100%" :loading="warehouseLoading" size="medium">
              <el-option v-for="item in warehouseTypes" :key="item.warehouseType" :label="item.warehouseTypeDesc"
                :value="item.warehouseType" />
            </el-select>
          </el-form-item>
        </el-form>
@@ -41,38 +20,26 @@
      <div class="location-section compact">
        <el-form :model="form" :rules="rules" ref="locationForm" class="compact-form">
          <el-form-item label="仓库区域" prop="locationType" class="location-select compact-item">
            <el-select
              v-model="form.locationType"
              placeholder="请先选择仓库"
              clearable
              filterable
              @change="handleLocationChange"
              style="width: 100%"
              :loading="locationLoading"
              size="medium"
            >
              <el-option
                v-for="item in locationTypes"
                :key="item.locationType"
                :label="item.locationTypeDesc"
                :value="item.locationType"
              />
            <el-select v-model="form.locationType" placeholder="请先选择仓库" clearable filterable
              @change="handleLocationChange" style="width: 100%" :loading="locationLoading" size="medium">
              <el-option v-for="item in locationTypes" :key="item.locationType" :label="item.locationTypeDesc"
                :value="item.locationType" />
            </el-select>
          </el-form-item>
        </el-form>
      </div>
      <!-- æ‰˜ç›˜ä¿¡æ¯æ˜¾ç¤º - ç´§å‡‘布局 -->
      <div class="tray-info compact" v-if="trayBarcode">
        <i class="el-icon-s-management"></i> å½“前料箱: {{ trayBarcode }}
        <span class="location-info" v-if="form.warehouseType">
          | ä»“库: {{ currentWarehouseName  }}
          | ä»“库: {{ currentWarehouseName }}
        </span>
        <span class="location-info" v-if="form.locationType">
          | ä»“库区域: {{ currentLocationDesc }}
        </span>
      </div>
      <!-- æ‰«ç åŒºåŸŸ - ç´§å‡‘布局 -->
      <div class="input-section compact">
        <el-card shadow="hover" class="compact-card">
@@ -83,73 +50,45 @@
              {{ form.locationType && form.warehouseType ? '扫码就绪' : '请先选择仓库和仓库区域' }}
            </span>
          </div>
          <!-- æ‰˜ç›˜æ¡ç è¾“å…¥ -->
          <div class="input-wrapper custom-input-group compact-input">
            <div class="input-label">料箱码</div>
            <el-input
              ref="trayInput"
              v-model="trayBarcode"
              placeholder="请扫描或输入料箱码后按回车键"
              clearable
              :disabled="!form.locationType || !form.warehouseType"
              @keyup.enter.native="handleTraySubmit"
              @clear="handleTrayClear"
              @input="handleTrayInput"
              class="custom-input"
              size="medium"
            >
            <el-input ref="trayInput" v-model="trayBarcode" placeholder="请扫描或输入料箱码后按回车键" clearable
              :disabled="!form.locationType || !form.warehouseType" @keyup.enter.native="handleTraySubmit"
              @clear="handleTrayClear" @input="handleTrayInput" class="custom-input" size="medium">
              <template slot="append">
                <el-button
                  @click="handleTraySubmit"
                  type="primary"
                  icon="el-icon-position"
                  :disabled="!form.locationType || !trayBarcode || !form.warehouseType"
                  size="medium"
                >
                <el-button @click="handleTraySubmit" type="primary" icon="el-icon-position"
                  :disabled="!form.locationType || !trayBarcode || !form.warehouseType" size="medium">
                  ç¡®è®¤
                </el-button>
              </template>
            </el-input>
          </div>
          <!-- ç‰©æ–™æ¡ç è¾“å…¥ -->
          <div class="input-wrapper custom-input-group compact-input">
            <div class="input-label">物料条码</div>
            <el-input
              ref="barcodeInput"
              v-model="barcode"
              placeholder="请扫描或输入物料条码后按回车键"
              clearable
            <el-input ref="barcodeInput" v-model="barcode" placeholder="请扫描或输入物料条码后按回车键" clearable
              :disabled="!form.locationType || !trayBarcode || !form.warehouseType"
              @keyup.enter.native="handleBarcodeSubmit"
              @clear="handleClear"
              @input="handleBarcodeInput"
              class="custom-input"
              size="medium"
            >
              @keyup.enter.native="handleBarcodeSubmit" @clear="handleClear" @input="handleBarcodeInput"
              class="custom-input" size="medium">
              <template slot="append">
                <el-button
                  :loading="loading"
                  @click="handleBarcodeSubmit"
                  type="primary"
                  icon="el-icon-search"
                  :disabled="!form.locationType || !trayBarcode || !barcode || !from.warehouseType"
                  size="medium"
                >
                <el-button :loading="loading" @click="handleBarcodeSubmit" type="primary" icon="el-icon-search"
                  :disabled="!form.locationType || !trayBarcode || !barcode || !from.warehouseType" size="medium">
                  {{ loading ? '查询中...' : '查询' }}
                </el-button>
              </template>
            </el-input>
          </div>
          <div class="input-tips compact-tips">
            <p>提示:请先选择仓库 â†’ é€‰æ‹©ä»“库区域 â†’ è¾“入料箱码 â†’ è¾“入物料条码</p>
            <p v-if="!form.warehouseType" class="warning-text">⚠️ è¯·å…ˆé€‰æ‹©ä»“库</p>
            <p v-if="!form.locationType && !form.warehouseType" class="warning-text">⚠️ è¯·å…ˆé€‰æ‹©ä»“库区域</p>
            <p v-if="form.warehouseType && form.locationType && !trayBarcode" class="warning-text">⚠️ è¯·å…ˆè¾“入料箱码</p>
          </div>
        </el-card>
      </div>
@@ -161,13 +100,7 @@
      <!-- é”™è¯¯æç¤º -->
      <div v-if="error" class="error-message compact">
        <el-alert
          :title="error"
          type="error"
          show-icon
          closable
          @close="error = ''"
        />
        <el-alert :title="error" type="error" show-icon closable @close="error = ''" />
      </div>
      <!-- ç‰©æ–™åˆ—表 - å›ºå®šé«˜åº¦å¸¦æ»šåŠ¨æ¡ -->
@@ -184,7 +117,7 @@
              <el-tag v-if="form.locationType" type="info" size="small">区域: {{ currentLocationDesc }}</el-tag>
            </span>
          </div>
          <div v-if="materials.length === 0" class="empty-state compact">
            <i class="el-icon-document"></i>
            <p v-if="!form.warehouseType">请先选择仓库</p>
@@ -192,15 +125,9 @@
            <p v-else-if="!trayBarcode">请先输入料箱条码</p>
            <p v-else>暂无物料数据,请扫描或输入物料条码</p>
          </div>
          <div class="table-container" v-else>
            <el-table
              :data="materials"
              stripe
              style="width: 100%"
              height="100%"
              size="small"
            >
            <el-table :data="materials" stripe style="width: 100%" height="100%" size="small">
              <el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
              <el-table-column prop="barcode" label="条码" min-width="140" show-overflow-tooltip></el-table-column>
              <el-table-column prop="materielCode" label="物料编码" min-width="150" show-overflow-tooltip></el-table-column>
@@ -214,19 +141,19 @@
        </el-card>
      </div>
    </div>
   <!--      <div slot="footer" class="dialog-footer">
    <!--      <div slot="footer" class="dialog-footer">
      <el-button @click="handleCancel">取消</el-button>
      <el-button type="primary" @click="handleConfirm">确认</el-button>
    </div> -->
    </vol-box>
  </vol-box>
</template>
<script>
import http from '@/api/http.js';
import VolBox from '@/components/basic/VolBox.vue';
import VolForm from '@/components/basic/VolForm.vue';
import VolTable from '@/components/basic/VolTable.vue';
import { ElLoading, ElMessage,ElMessageBox  } from 'element-plus';
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus';
import { ref, onMounted, onUnmounted } from 'vue'
import InboundOrder from '../../../views/inbound/inboundOrder.vue';
import { th } from 'element-plus/es/locales.mjs';
@@ -241,50 +168,50 @@
  data() {
    return {
        palletVisible: this.visible,
         trayBarcode: '',
          barcode: '',
          materials: [],
          loading: false,
          error: '',
          debugMode: false,
          currentFocus: 'warehouse',
          // æ‰«ç æžªç›¸å…³å˜é‡
          scanCode: '',
          lastKeyTime: null,
          isManualInput: false,
          isScanning: false,
          scanTimer: null,
          manualInputTimer: null,
          scanTarget: 'tray', // å½“前扫码目标: tray æˆ– material
      palletVisible: this.visible,
      trayBarcode: '',
      barcode: '',
      materials: [],
      loading: false,
      error: '',
      debugMode: false,
      currentFocus: 'warehouse',
          // åº“存统计相关变量
          totalStockSum: 0,
          totalStockCount: 0,
          uniqueUnit: '',
          sumLoading: false,
          sumError: '',
        // ä»“库相关变量
          warehouseTypes: [],
          warehouseLoading: false,
        // ä»“库区域相关变量
        locationTypes: [],
        locationLoading: false,
        form: {
            warehouseType: null,
            locationType: null
        },
    rules: {
      locationType: [
        {
          validator: this.validateLocationType,
          trigger: 'change'
        }
      ],
      warehouseType: [
          {
            massage:'请选择仓库',
      // æ‰«ç æžªç›¸å…³å˜é‡
      scanCode: '',
      lastKeyTime: null,
      isManualInput: false,
      isScanning: false,
      scanTimer: null,
      manualInputTimer: null,
      scanTarget: 'tray', // å½“前扫码目标: tray æˆ– material
      // åº“存统计相关变量
      totalStockSum: 0,
      totalStockCount: 0,
      uniqueUnit: '',
      sumLoading: false,
      sumError: '',
      // ä»“库相关变量
      warehouseTypes: [],
      warehouseLoading: false,
      // ä»“库区域相关变量
      locationTypes: [],
      locationLoading: false,
      form: {
        warehouseType: null,
        locationType: null
      },
      rules: {
        locationType: [
          {
            validator: this.validateLocationType,
            trigger: 'change'
          }
        ],
        warehouseType: [
          {
            massage: '请选择仓库',
            trigger: 'change'
          }
        ]
@@ -297,44 +224,44 @@
      set(newVal) { this.$emit('update:visible', newVal); }
    },
    currentDocNo() { return this.docNo; },
        // å½“前选择的仓库名称
    // å½“前选择的仓库名称
    currentWarehouseName() {
      const warehouse = this.warehouseTypes.find(item => item.warehouseType === this.form.warehouseType);
      return warehouse ? warehouse.warehouseTypeDesc : '';
    },
        // å½“前选择的仓库区域描述
    // å½“前选择的仓库区域描述
    currentLocationDesc() {
        const location = this.locationTypes.find(item => item.locationType === this.form.locationType)
        return location ? location.locationTypeDesc : ''
      const location = this.locationTypes.find(item => item.locationType === this.form.locationType)
      return location ? location.locationTypeDesc : ''
    }
  },
  watch: {
    visible(newVal, oldVal) {
    this.palletVisible = newVal;
    // å½“从 false å˜ä¸º true æ—¶ï¼Œè¡¨ç¤ºå¼¹æ¡†æ‰“å¼€
    if (newVal === true && oldVal === false) {
      console.log('弹框打开,重置数据');
      this.resetData();
      this.$nextTick(() => {
        setTimeout(() => {
         // this.focusTrayInput();
          this.initwarehouseTypes(); // åˆå§‹åŒ–仓库
          this.initLocationTypes(); // åˆå§‹åŒ–仓库区域
          this.fetchStockStatistics(); // åŠ è½½ç»Ÿè®¡æ•°æ®
        }, 300);
      });
    }
    // å½“从 true å˜ä¸º false æ—¶ï¼Œè¡¨ç¤ºå¼¹æ¡†å…³é—­
    if (newVal === false && oldVal === true) {
      console.log('弹框关闭,重置数据');
      this.resetData();
    }
  },
  palletVisible(newVal) {
    this.$emit('update:visible', newVal);
  },
      this.palletVisible = newVal;
      // å½“从 false å˜ä¸º true æ—¶ï¼Œè¡¨ç¤ºå¼¹æ¡†æ‰“å¼€
      if (newVal === true && oldVal === false) {
        console.log('弹框打开,重置数据');
        this.resetData();
        this.$nextTick(() => {
          setTimeout(() => {
            // this.focusTrayInput();
            this.initwarehouseTypes(); // åˆå§‹åŒ–仓库
            this.initLocationTypes(); // åˆå§‹åŒ–仓库区域
            this.fetchStockStatistics(); // åŠ è½½ç»Ÿè®¡æ•°æ®
          }, 300);
        });
      }
      // å½“从 true å˜ä¸º false æ—¶ï¼Œè¡¨ç¤ºå¼¹æ¡†å…³é—­
      if (newVal === false && oldVal === true) {
        console.log('弹框关闭,重置数据');
        this.resetData();
      }
    },
    palletVisible(newVal) {
      this.$emit('update:visible', newVal);
    },
    docNo(newVal) {
      if (newVal) {
        this.palletForm = { palletCode: '', barcode: '' };
@@ -345,160 +272,160 @@
    }
  },
  'form.warehouseType'(newVal) {
      if (newVal) {
        this.form.locationType = null;
    if (newVal) {
      this.form.locationType = null;
    } else {
      this.locationTypes = [];
    }
  },
  mounted() {
    // æ·»åŠ å…¨å±€é”®ç›˜ç›‘å¬
    document.addEventListener('keypress', this.handleKeyPress);
    // ä½¿ç”¨setTimeout确保DOM完全渲染后再聚焦
    setTimeout(() => {
      // this.focusTrayInput();
      this.focusLocationSelect();
    }, 300);
  },
  beforeDestroy() {
    // æ¸…理事件监听
    document.removeEventListener('keypress', this.handleKeyPress);
    this.clearAllTimers();
  },
  methods: {
    /**
* è‡ªå®šä¹‰ä»“库区域验证
* å…è®¸å€¼ä¸º0,因为0是合法的locationType
*/
    validateLocationType(rule, value, callback) {
      // æ£€æŸ¥å€¼æ˜¯å¦ä¸ºnull、undefined或空字符串,但允许数字0
      if (!this.form.warehouseType) {
        callback(new Error('请先选择仓库'));
      } else if (value === null || value === undefined || value === '') {
        callback(new Error('请选择仓库区域'));
      } else {
        this.locationTypes = [];
        callback();
      }
    },
    /**
    * åˆå§‹åŒ–仓库区域数据
    */
    async initLocationTypes() {
      this.locationLoading = true;
      this.error = '';
      try {
        const response = await http.post('/api/LocationInfo/GetLocationTypes');
        if (response.status && Array.isArray(response.data)) {
          this.locationTypes = response.data;
          if (this.locationTypes.length === 0) {
            this.error = '未获取到仓库区域数据';
          } else {
            // å¦‚果有默认区域,可以在这里设置
            // this.form.locationType = this.locationTypes[0].locationType;
          }
        } else {
          this.error = '获取仓库区域数据失败';
        }
      } catch (error) {
        console.error('获取仓库区域失败:', error);
        this.error = `获取仓库区域失败: ${error.message || '网络错误'}`;
      } finally {
        this.locationLoading = false;
      }
    },
 mounted() {
        // æ·»åŠ å…¨å±€é”®ç›˜ç›‘å¬
        document.addEventListener('keypress', this.handleKeyPress);
        // ä½¿ç”¨setTimeout确保DOM完全渲染后再聚焦
        setTimeout(() => {
         // this.focusTrayInput();
                  this.focusLocationSelect();
        }, 300);
      },
      beforeDestroy() {
        // æ¸…理事件监听
        document.removeEventListener('keypress', this.handleKeyPress);
         this.clearAllTimers();
      },
      methods: {
         /**
   * è‡ªå®šä¹‰ä»“库区域验证
   * å…è®¸å€¼ä¸º0,因为0是合法的locationType
   */
  validateLocationType(rule, value, callback) {
    // æ£€æŸ¥å€¼æ˜¯å¦ä¸ºnull、undefined或空字符串,但允许数字0
    if (!this.form.warehouseType) {
      callback(new Error('请先选择仓库'));
    } else if (value === null || value === undefined || value === '') {
      callback(new Error('请选择仓库区域'));
    } else {
      callback();
    }
  },
         /**
         * åˆå§‹åŒ–仓库区域数据
         */
        async initLocationTypes() {
            this.locationLoading = true;
            this.error = '';
            try {
                const response = await http.post('/api/LocationInfo/GetLocationTypes');
                if (response.status && Array.isArray(response.data)) {
                    this.locationTypes = response.data;
                    if (this.locationTypes.length === 0) {
                        this.error = '未获取到仓库区域数据';
                    } else {
                        // å¦‚果有默认区域,可以在这里设置
                        // this.form.locationType = this.locationTypes[0].locationType;
                    }
                } else {
                    this.error = '获取仓库区域数据失败';
                }
            } catch (error) {
                console.error('获取仓库区域失败:', error);
                this.error = `获取仓库区域失败: ${error.message || '网络错误'}`;
            } finally {
                this.locationLoading = false;
            }
        },
    /**
     * åˆå§‹åŒ–仓库数据
     */
    async initwarehouseTypes() {
      this.warehouseLoading = true;
      this.error = '';
        /**
         * åˆå§‹åŒ–仓库数据
         */
        async initwarehouseTypes() {
            this.warehouseLoading = true;
            this.error = '';
            try {
                const response = await http.post('/api/Warehouse/GetwarehouseTypes');
                if (response.status && Array.isArray(response.data)) {
                    this.warehouseTypes = response.data;
                    if (this.warehouseTypes.length === 0) {
                        this.error = '未获取到仓库数据';
                    } else {
                        // å¦‚果有默认区域,可以在这里设置
                        // this.form.locationType = this.locationTypes[0].locationType;
                    }
                } else {
                    this.error = '获取仓库数据失败';
                }
            } catch (error) {
                console.error('获取仓库失败:', error);
                this.error = `获取仓库失败: ${error.message || '网络错误'}`;
            } finally {
                this.warehouseLoading = false;
            }
        },
      try {
        const response = await http.post('/api/Warehouse/GetwarehouseTypes');
        if (response.status && Array.isArray(response.data)) {
          this.warehouseTypes = response.data;
          if (this.warehouseTypes.length === 0) {
            this.error = '未获取到仓库数据';
          } else {
            // å¦‚果有默认区域,可以在这里设置
            // this.form.locationType = this.locationTypes[0].locationType;
          }
        } else {
          this.error = '获取仓库数据失败';
        }
      } catch (error) {
        console.error('获取仓库失败:', error);
        this.error = `获取仓库失败: ${error.message || '网络错误'}`;
      } finally {
        this.warehouseLoading = false;
      }
    },
 /**
 * ä»“库区域变更处理
 */
handleLocationChange(value) {
 console.log('选择仓库区域:', value, '类型:', typeof value, this.currentLocationDesc);
    // ç«‹å³æ¸…除错误信息
    this.error = '';
    // æ‰‹åŠ¨è§¦å‘è¡¨å•éªŒè¯æ›´æ–°
    this.$nextTick(() => {
      if (this.$refs.locationForm) {
        // æ¸…除该字段的验证状态,然后重新验证
        this.$refs.locationForm.clearValidate('locationType');
        // çŸ­æš‚延迟后重新验证,确保DOM已更新
        setTimeout(() => {
          this.$refs.locationForm.validateField('locationType', (errorMsg) => {
            if (!errorMsg && (value === 0 || value)) {
              console.log('仓库区域验证通过:', value);
              // åŒºåŸŸé€‰æ‹©åŽï¼Œè‡ªåŠ¨èšç„¦åˆ°æ‰˜ç›˜è¾“å…¥æ¡†
              this.focusLocationSelect();
            }
          });
        }, 100);
    }
  });
},
    /**
    * ä»“库区域变更处理
    */
    handleLocationChange(value) {
      console.log('选择仓库区域:', value, '类型:', typeof value, this.currentLocationDesc);
/**
 * ä»“库变更处理
 */
handleWarehouseChange(value) {
 console.log('选择仓库:', value, '类型:', typeof value, this.currentWarehouseName);
    // ç«‹å³æ¸…除错误信息
    this.error = '';
    // æ‰‹åŠ¨è§¦å‘è¡¨å•éªŒè¯æ›´æ–°
    this.$nextTick(() => {
      if (this.$refs.locationForm) {
        // æ¸…除该字段的验证状态,然后重新验证
        this.$refs.locationForm.clearValidate('warehouseType');
        // çŸ­æš‚延迟后重新验证,确保DOM已更新
        setTimeout(() => {
          this.$refs.locationForm.validateField('warehouseType', (errorMsg) => {
            if (!errorMsg && (value === 0 || value)) {
              console.log('仓库验证通过:', value);
              this.focusLocationSelect();
            }
          });
        }, 100);
    }
  });
},
      // ç«‹å³æ¸…除错误信息
      this.error = '';
      // æ‰‹åŠ¨è§¦å‘è¡¨å•éªŒè¯æ›´æ–°
      this.$nextTick(() => {
        if (this.$refs.locationForm) {
          // æ¸…除该字段的验证状态,然后重新验证
          this.$refs.locationForm.clearValidate('locationType');
          // çŸ­æš‚延迟后重新验证,确保DOM已更新
          setTimeout(() => {
            this.$refs.locationForm.validateField('locationType', (errorMsg) => {
              if (!errorMsg && (value === 0 || value)) {
                console.log('仓库区域验证通过:', value);
                // åŒºåŸŸé€‰æ‹©åŽï¼Œè‡ªåŠ¨èšç„¦åˆ°æ‰˜ç›˜è¾“å…¥æ¡†
                this.focusLocationSelect();
              }
            });
          }, 100);
        }
      });
    },
    /**
     * ä»“库变更处理
     */
    handleWarehouseChange(value) {
      console.log('选择仓库:', value, '类型:', typeof value, this.currentWarehouseName);
      // ç«‹å³æ¸…除错误信息
      this.error = '';
      // æ‰‹åŠ¨è§¦å‘è¡¨å•éªŒè¯æ›´æ–°
      this.$nextTick(() => {
        if (this.$refs.locationForm) {
          // æ¸…除该字段的验证状态,然后重新验证
          this.$refs.locationForm.clearValidate('warehouseType');
          // çŸ­æš‚延迟后重新验证,确保DOM已更新
          setTimeout(() => {
            this.$refs.locationForm.validateField('warehouseType', (errorMsg) => {
              if (!errorMsg && (value === 0 || value)) {
                console.log('仓库验证通过:', value);
                this.focusLocationSelect();
              }
            });
          }, 100);
        }
      });
    },
    async fetchStockStatistics() {
      // å•据号为空时不查询
      if (!this.docNo) {
@@ -510,8 +437,8 @@
      this.sumError = '';
      try {
        // è°ƒç”¨åŽç«¯ç»Ÿè®¡æŽ¥å£ï¼ˆæ›¿æ¢ä¸ºä½ çš„实际接口路径)
        const response = await http.post('/api/InboundOrder/UnPalletQuantity?orderNo='+this.docNo, {
        const response = await http.post('/api/InboundOrder/UnPalletQuantity?orderNo=' + this.docNo, {
        });
        // ç»‘定数据(匹配 PalletSumQuantityDTO ç»“构)
@@ -529,82 +456,91 @@
        this.sumLoading = false;
      }
    },
/**
 * è¡¨å•验证
 */
async validateForm() {
  return new Promise((resolve) => {
    if (!this.$refs.locationForm) {
      this.error = '表单未初始化';
      this.$message.warning('请先选择仓库区域');
      resolve(false);
      return;
    }
    /**
     * è¡¨å•验证
     */
    async validateForm() {
      return new Promise((resolve) => {
        if (!this.$refs.locationForm) {
          this.error = '表单未初始化';
          this.$message.warning('请先选择仓库区域');
          resolve(false);
          return;
        }
    this.$refs.locationForm.validate((valid) => {
      if (valid) {
        this.error = '';
        resolve(true);
      } else {
        // æ‰‹åŠ¨æ£€æŸ¥locationType,正确处理值为0的情况
        if(!this.from.warehouseType){
          this.error='请先选择仓库';
        this.$refs.locationForm.validate((valid) => {
          if (valid) {
            this.error = '';
            resolve(true);
          } else {
            // æ‰‹åŠ¨æ£€æŸ¥locationType,正确处理值为0的情况
            if (!this.from.warehouseType) {
              this.error = '请先选择仓库';
            }
            else if (this.form.locationType === null || this.form.locationType === undefined || this.form.locationType === '') {
              this.error = '请先选择仓库区域';
              //this.$message.warning('请先选择仓库区域');
            } else {
              // å¦‚果值存在(包括0),但验证不通过,可能是其他验证错误
              this.error = '请检查表单填写是否正确';
            }
            resolve(false);
          }
        });
      });
    },
    focusWarehouseSelect() {
      if (this.$refs.locationForm) {
        const selectEl = this.$el.querySelector('.location-select:first-child .el-input__inner');
        if (selectEl) {
          selectEl.focus();
          this.currentFocus = 'warehouse';
        }
        else if(this.form.locationType === null || this.form.locationType === undefined || this.form.locationType === '') {
          this.error = '请先选择仓库区域';
          //this.$message.warning('请先选择仓库区域');
        } else {
          // å¦‚果值存在(包括0),但验证不通过,可能是其他验证错误
          this.error = '请检查表单填写是否正确';
        }
        resolve(false);
      }
    });
  });
},
       focusWarehouseSelect() {
            if (this.$refs.locationForm) {
                const selectEl = this.$el.querySelector('.location-select:first-child .el-input__inner');
                if (selectEl) {
                    selectEl.focus();
                    this.currentFocus = 'warehouse';
                }
            }
        },
        // èšç„¦åˆ°ä»“库区域选择
        focusLocationSelect() {
            if (this.$refs.locationForm) {
                const selectEl = this.$el.querySelector('.location-select:nth-child(2) .el-input__inner');
                if (selectEl) {
                    selectEl.focus();
                    this.currentFocus = 'location';
                }
            }
        },
        // èšç„¦åˆ°æ‰˜ç›˜è¾“入框
        focusTrayInput() {
            if (this.$refs.trayInput && this.$refs.trayInput.$el) {
                const inputEl = this.$refs.trayInput.$el.querySelector('input');
                if (inputEl) {
                    inputEl.focus();
                    this.currentFocus = 'tray';
                    this.scanTarget = 'tray';
                }
            }
        },
        // èšç„¦åˆ°ç‰©æ–™è¾“入框
        focusBarcodeInput() {
            if (this.$refs.barcodeInput && this.$refs.barcodeInput.$el) {
                const inputEl = this.$refs.barcodeInput.$el.querySelector('input');
                if (inputEl) {
                    inputEl.focus();
                    this.currentFocus = 'material';
                    this.scanTarget = 'material';
                }
            }
        },
         // é‡ç½®æ‰€æœ‰æ•°æ®
    },
    // èšç„¦åˆ°ä»“库区域选择
    focusLocationSelect() {
      if (this.$refs.locationForm) {
        const selectEl = this.$el.querySelector('.location-select:nth-child(2) .el-input__inner');
        if (selectEl) {
          selectEl.focus();
          this.currentFocus = 'location';
        }
      }
    },
    // èšç„¦åˆ°æ‰˜ç›˜è¾“入框
    focusTrayInput() {
      if (this.$refs.trayInput && this.$refs.trayInput.$el) {
        const inputEl = this.$refs.trayInput.$el.querySelector('input');
        if (inputEl) {
          inputEl.focus();
          this.currentFocus = 'tray';
          this.scanTarget = 'tray';
        }
      }
    },
    // èšç„¦åˆ°ç‰©æ–™è¾“入框
    focusBarcodeInput() {
      if (this.$refs.barcodeInput && this.$refs.barcodeInput.$el) {
        const inputEl = this.$refs.barcodeInput.$el.querySelector('input');
        if (inputEl) {
          inputEl.focus();
          this.currentFocus = 'material';
          this.scanTarget = 'material';
            inputEl.select();
            console.log('物料输入框内容已选中');
        }
      }
    },
    // é‡ç½®æ‰€æœ‰æ•°æ®
    resetData() {
      console.log('重置弹框数据');
      this.trayBarcode = '';
@@ -623,20 +559,20 @@
      this.totalStockCount = 0;
      this.sumLoading = false;
      this.sumError = '';
        this.form={
          warehouseType:null,
          locationType:null
      this.form = {
        warehouseType: null,
        locationType: null
      }
      this.warehouseTypes = [];
      this.locationTypes = [];
      // æ¸…除表单验证状态
      this.$nextTick(() => {
        if (this.$refs.locationForm) {
          this.$refs.locationForm.clearValidate();
        }
      this.warehouseTypes=[];
      this.locationTypes=[];
          // æ¸…除表单验证状态
  this.$nextTick(() => {
    if (this.$refs.locationForm) {
      this.$refs.locationForm.clearValidate();
    }
  });
      });
    },
    // æ¸…除所有计时器
    clearAllTimers() {
      if (this.manualInputTimer) {
@@ -648,7 +584,7 @@
        this.scanTimer = null;
      }
    },
    // å¼¹æ¡†æ‰“开时重置数据
    handleDialogOpen() {
      console.log('弹框打开,重置数据');
@@ -656,676 +592,691 @@
      // ä½¿ç”¨setTimeout确保DOM完全渲染后再聚焦
      this.$nextTick(() => {
        setTimeout(() => {
            this.initwarehouseTypes();
            this.initLocationTypes(); // åˆå§‹åŒ–仓库区域
             // ç¡®ä¿è¡¨å•引用存在后再聚焦
      if (this.$refs.locationForm) {
        this.focusWarehouseSelect();
      } else {
        // å¦‚果表单引用还不存在,稍后重试
        setTimeout(() => {
          this.focusWarehouseSelect();
        }, 500);
      }
          this.initwarehouseTypes();
          this.initLocationTypes(); // åˆå§‹åŒ–仓库区域
          // ç¡®ä¿è¡¨å•引用存在后再聚焦
          if (this.$refs.locationForm) {
            this.focusWarehouseSelect();
          } else {
            // å¦‚果表单引用还不存在,稍后重试
            setTimeout(() => {
              this.focusWarehouseSelect();
            }, 500);
          }
        }, 300);
      });
    },
    // å¼¹æ¡†å…³é—­æ—¶é‡ç½®æ•°æ®
    handleDialogClose() {
      console.log('弹框关闭,重置数据');
      this.resetData();
    },
    // å–消按钮
    handleCancel() {
      this.palletVisible = false;
    },
    // ç¡®è®¤æŒ‰é’®
   async  handleConfirm() {
           if (!await this.validateForm()) return;
    async handleConfirm() {
      if (!await this.validateForm()) return;
      if (this.materials.length === 0) {
        this.$message.warning('请至少添加一个物料');
        return;
      }
      if (!this.trayBarcode) {
        this.$message.warning('请输入托盘条码');
        return;
      }
      const result = {
        warehouseType:this.form.warehouseType,
        warehouseName:this.currentWarehouseName,
        warehouseType: this.form.warehouseType,
        warehouseName: this.currentWarehouseName,
        locationType: this.form.locationType,
        locationDesc: this.currentLocationDesc,
        trayBarcode: this.trayBarcode,
        materials: this.materials,
        docNo: this.docNo
      };
      // è§¦å‘父组件的 back-success äº‹ä»¶
      this.$emit('back-success', result);
      this.palletVisible = false;
    },
    // å¤„理托盘输入
        handleTrayInput() {
            // æ ‡è®°ä¸ºæ‰‹åŠ¨è¾“å…¥æ¨¡å¼
            this.isManualInput = true;
            this.isScanning = false;
            // æ¸…除之前的计时器
            if (this.manualInputTimer) {
                clearTimeout(this.manualInputTimer);
            }
            // è®¾ç½®è®¡æ—¶å™¨ï¼Œå¦‚果一段时间内没有输入,则重置为扫码模式
            this.manualInputTimer = setTimeout(() => {
                this.isManualInput = false;
            }, 1000);
        },
        // å¤„理物料输入
        handleBarcodeInput() {
            // æ ‡è®°ä¸ºæ‰‹åŠ¨è¾“å…¥æ¨¡å¼
            this.isManualInput = true;
            this.isScanning = false;
            // æ¸…除之前的计时器
            if (this.manualInputTimer) {
                clearTimeout(this.manualInputTimer);
            }
            // è®¾ç½®è®¡æ—¶å™¨ï¼Œå¦‚果一段时间内没有输入,则重置为扫码模式
            this.manualInputTimer = setTimeout(() => {
                this.isManualInput = false;
            }, 1000);
        },
       // å¤„理托盘条码提交
async handleTraySubmit() {
  // å…ˆç›´æŽ¥æ£€æŸ¥locationType,避免表单验证的异步问题
  if (!this.form.warehouseType) {
    this.error = '请先选择仓库';
    return;
  }
  if (!this.form.locationType) {
    this.error = '请先选择仓库区域';
    //this.$message.warning('请先选择仓库区域');
    return;
  }
  // ç„¶åŽå†è¿›è¡Œå®Œæ•´çš„表单验证
  if (!await this.validateForm()) return;
  const currentTrayBarcode = this.trayBarcode.trim();
  if (!currentTrayBarcode) {
    this.error = '请输入或扫描托盘条码';
    return;
  }
    handleTrayInput() {
      // æ ‡è®°ä¸ºæ‰‹åŠ¨è¾“å…¥æ¨¡å¼
      this.isManualInput = true;
      this.isScanning = false;
  this.error = '';
  // è®¾ç½®æ‰˜ç›˜æ¡ç åŽï¼Œè‡ªåŠ¨èšç„¦åˆ°ç‰©æ–™è¾“å…¥æ¡†
  this.focusBarcodeInput();
  this.$message({
    message: `托盘条码已设置: ${currentTrayBarcode}`,
    type: 'success',
    duration: 2000
  });
},
        // æ¸…除托盘
        clearTray() {
          this.trayBarcode = '';
          this.materials = [];
          this.focusTrayInput();
      // æ¸…除之前的计时器
      if (this.manualInputTimer) {
        clearTimeout(this.manualInputTimer);
      }
      // è®¾ç½®è®¡æ—¶å™¨ï¼Œå¦‚果一段时间内没有输入,则重置为扫码模式
      this.manualInputTimer = setTimeout(() => {
        this.isManualInput = false;
      }, 1000);
    },
    // å¤„理物料输入
    handleBarcodeInput() {
      // æ ‡è®°ä¸ºæ‰‹åŠ¨è¾“å…¥æ¨¡å¼
      this.isManualInput = true;
      this.isScanning = false;
      // æ¸…除之前的计时器
      if (this.manualInputTimer) {
        clearTimeout(this.manualInputTimer);
      }
      // è®¾ç½®è®¡æ—¶å™¨ï¼Œå¦‚果一段时间内没有输入,则重置为扫码模式
      this.manualInputTimer = setTimeout(() => {
        this.isManualInput = false;
      }, 1000);
    },
    // å¤„理托盘条码提交
    async handleTraySubmit() {
      // å…ˆç›´æŽ¥æ£€æŸ¥locationType,避免表单验证的异步问题
      if (!this.form.warehouseType) {
        this.error = '请先选择仓库';
        return;
      }
      if (!this.form.locationType) {
        this.error = '请先选择仓库区域';
        //this.$message.warning('请先选择仓库区域');
        return;
      }
      // ç„¶åŽå†è¿›è¡Œå®Œæ•´çš„表单验证
      if (!await this.validateForm()) return;
      const currentTrayBarcode = this.trayBarcode.trim();
      if (!currentTrayBarcode) {
        this.error = '请输入或扫描托盘条码';
        return;
      }
      this.error = '';
      // è®¾ç½®æ‰˜ç›˜æ¡ç åŽï¼Œè‡ªåŠ¨èšç„¦åˆ°ç‰©æ–™è¾“å…¥æ¡†
      this.focusBarcodeInput();
      this.$message({
        message: `托盘条码已设置: ${currentTrayBarcode}`,
        type: 'success',
        duration: 2000
      });
    },
    // æ¸…除托盘
    clearTray() {
      this.trayBarcode = '';
      this.materials = [];
      this.focusTrayInput();
      this.$message({
        message: '托盘条码已清除',
        type: 'info',
        duration: 2000
      });
    },
    // æ¸…空托盘输入
    handleTrayClear() {
      this.error = '';
    },
    // æ¸…空输入
    handleClear() {
      this.error = '';
      this.scanCode = '';
      this.isManualInput = false;
      this.isScanning = false;
    },
    // å¤„理物料条码提交
    async handleBarcodeSubmit() {
      if (!await this.validateForm()) return;
      const currentBarcode = this.barcode.trim();
      if (!this.trayBarcode) {
        this.error = '请先输入托盘条码';
        this.focusTrayInput();
        return;
      }
      if (!currentBarcode) {
        this.error = '请输入或扫描物料条码';
        return;
      }
      this.focusBarcodeInput();
      this.error = '';
      this.loading = true;
      try {
        // è°ƒç”¨API查询物料信息
        const materialData = await this.fetchMaterialData(currentBarcode);
        if (!materialData || materialData.length === 0) {
          return;
        }
        // æ£€æŸ¥æ˜¯å¦å·²å­˜åœ¨ç›¸åŒç‰©æ–™ç¼–码的记录
        const exists = this.materials.some(item =>
          item.barcode === this.trayBarcode
        );
        console.log('API:', materialData)
        if (exists) {
          this.$message({
            message: '托盘条码已清除',
            type: 'info',
            message: '该条码已存在当前托盘的列表中',
            type: 'warning',
            duration: 2000
          });
        },
        // æ¸…空托盘输入
        handleTrayClear() {
          this.error = '';
        },
        // æ¸…空输入
        handleClear() {
          this.error = '';
          this.scanCode = '';
          this.isManualInput = false;
          this.isScanning = false;
        },
        // å¤„理物料条码提交
        async handleBarcodeSubmit() {
                    if (!await this.validateForm()) return;
          const currentBarcode = this.barcode.trim();
          
          if (!this.trayBarcode) {
            this.error = '请先输入托盘条码';
            this.focusTrayInput();
            return;
          }
          if (!currentBarcode) {
            this.error = '请输入或扫描物料条码';
            return;
          }
        } else {
          this.error = '';
          this.loading = true;
          materialData.forEach(item => {
          try {
            // è°ƒç”¨API查询物料信息
            const materialData = await this.fetchMaterialData(currentBarcode);
                  if (!materialData  || materialData.length === 0) {
          return;
         }
            // æ£€æŸ¥æ˜¯å¦å·²å­˜åœ¨ç›¸åŒç‰©æ–™ç¼–码的记录
            const exists = this.materials.some(item =>
                 item.barcode === this.trayBarcode
            );
            console.log('API:',materialData)
            if (exists) {
              this.$message({
                message: '该条码已存在当前托盘的列表中',
                type: 'warning',
                duration: 2000
              });
            } else {
                materialData.forEach(item => {
          // å¦‚果不存在,添加新物料
          this.materials.push({
            ...item,
             trayCode: this.trayBarcode,
               locationType: this.form.locationType,
                            locationDesc: this.currentLocationDesc,
               scanTime: this.formatTime(new Date())
            // å¦‚果不存在,添加新物料
            this.materials.push({
              ...item,
              trayCode: this.trayBarcode,
              locationType: this.form.locationType,
              locationDesc: this.currentLocationDesc,
              scanTime: this.formatTime(new Date())
            });
          });
        });
              this.$message({
                message: `成功添加条码: ${currentBarcode}`,
                type: 'success',
                duration: 2000
              });
            this.fetchStockStatistics();
            // æ¸…空物料输入框并保持聚焦
            this.barcode = '';
            this.scanCode = ''; // æ¸…空扫码缓存
            this.isScanning = false;
            setTimeout(() => {
              this.focusBarcodeInput();
            }, 100);
          }
          } catch (err) {
            this.error = err.message || '查询条码信息失败,请重试';
          } finally {
            this.loading = false;
          }
        },
        // API请求 - æ›¿æ¢ä¸ºå®žé™…çš„API调用
      async  fetchMaterialData(barcode) {
         try {
        const response = await http.post('/api/InboundOrder/BarcodeMaterielGroup',
           {
          this.$message({
            message: `成功添加条码: ${currentBarcode}`,
            type: 'success',
            duration: 2000
          });
          this.fetchStockStatistics();
          // æ¸…空物料输入框并保持聚焦
          this.barcode = '';
          this.scanCode = ''; // æ¸…空扫码缓存
          this.isScanning = false;
          setTimeout(() => {
            this.focusBarcodeInput();
          }, 100);
        }
      } catch (err) {
        this.error = err.message || '查询条码信息失败,请重试';
        this.focusBarcodeInput();
        setTimeout(() => {
      // é€‰ä¸­è¾“入框内的错误内容(确保focus完成后执行)
      const inputEl = this.$refs.barcodeInput?.$el?.querySelector('input');
      if (inputEl) inputEl.select();
    }, 100);
      } finally {
        this.loading = false;
      }
    },
    // API请求 - æ›¿æ¢ä¸ºå®žé™…çš„API调用
    async fetchMaterialData(barcode) {
      try {
        const response = await http.post('/api/InboundOrder/BarcodeMaterielGroup',
          {
            palletCode: this.trayBarcode,
            orderNo: this.docNo,
            barcodes: barcode,
            locationTypeDesc:  this.currentLocationDesc,
            locationTypeDesc: this.currentLocationDesc,
            locationType: this.form.locationType, // æ·»åŠ ä»“åº“åŒºåŸŸä¿¡æ¯
            warehouseType:this.form.warehouseType
          }
            warehouseType: this.form.warehouseType
          }
        );
        let materialData;
        if (typeof response.data === 'string') {
          try {
            materialData = JSON.parse(response.data);
          } catch (e) {
          }
        } else {
          // å¦‚果返回的是JSON对象,直接使用
          materialData = response.data;
        }
 if(!response.status){
   this.error = response.message || '查询条码信息失败,请重试';
 }
        if (!response.status) {
          this.error = response.message || '查询条码信息失败,请重试';
        }
        return  materialData;
        return materialData;
      } catch (error) {
        console.error('API调用失败:', error);
      }
        },
        // å¤„理扫码枪输入
        handleKeyPress(event) {
          // å¦‚果是手动输入模式,不处理扫码枪逻辑
          if (this.isManualInput) {
            return;
          }
          const key = event.key;
          const currentTime = new Date().getTime();
          // å¿½ç•¥ç›´æŽ¥æŒ‰ä¸‹çš„回车键(由handleBarcodeSubmit处理)
          if (key === 'Enter') {
            if (this.scanCode.length > 0) {
              // é˜»æ­¢é»˜è®¤å›žè½¦è¡Œä¸ºï¼Œé¿å…è¡¨å•提交
              event.preventDefault();
              // æ‰«ç å®Œæˆï¼Œè‡ªåŠ¨è§¦å‘æŸ¥è¯¢
              this.isScanning = false;
              // æ ¹æ®å½“前扫码目标设置相应的输入框值
              if (this.scanTarget === 'tray') {
                this.trayBarcode = this.scanCode;
                this.handleTraySubmit();
              } else if (this.scanTarget === 'material') {
                this.barcode = this.scanCode;
                this.handleBarcodeSubmit();
              }
            }
            this.scanCode = '';
            this.lastKeyTime = null;
            return;
          }
          // æž„建扫码内容(快速连续输入视为扫码)
          if (this.lastKeyTime && currentTime - this.lastKeyTime < 50) {
            this.scanCode += key;
            this.isScanning = true;
          } else {
            this.scanCode = key;
            this.isScanning = true;
          }
          // è®¾ç½®è®¡æ—¶å™¨ï¼Œå¦‚果一段时间内没有输入,则重置扫描状态
          if (this.scanTimer) {
            clearTimeout(this.scanTimer);
          }
          this.scanTimer = setTimeout(() => {
            this.isScanning = false;
          }, 100);
          this.lastKeyTime = currentTime;
        },
        // åˆ é™¤ç‰©æ–™
        removeMaterial(index) {
          this.$confirm('确定要删除这条物料记录吗?', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
          }).then(() => {
            this.materials.splice(index, 1);
            this.$message({
              type: 'success',
              message: '删除成功!'
            });
            this.fetchStockStatistics();
          }).catch(() => {
            // å–消删除
          });
        },
        // æ¸…空所有物料
        clearAllMaterials() {
          if (this.materials.length === 0) return;
          this.$confirm('确定要清空所有物料记录吗?', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
          }).then(() => {
            this.materials = [];
            this.$message({
              type: 'success',
              message: '已清空所有记录!'
            });
          }).catch(() => {
            // å–消清空
          });
        },
        // æ ¼å¼åŒ–æ—¶é—´
        formatTime(date) {
          const year = date.getFullYear();
          const month = String(date.getMonth() + 1).padStart(2, '0');
          const day = String(date.getDate()).padStart(2, '0');
          const hours = String(date.getHours()).padStart(2, '0');
          const minutes = String(date.getMinutes()).padStart(2, '0');
          const seconds = String(date.getSeconds()).padStart(2, '0');
          return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
        }
      }
    },
    // å¤„理扫码枪输入
    handleKeyPress(event) {
      // å¦‚果是手动输入模式,不处理扫码枪逻辑
      if (this.isManualInput) {
        return;
      }
      const key = event.key;
      const currentTime = new Date().getTime();
      // å¿½ç•¥ç›´æŽ¥æŒ‰ä¸‹çš„回车键(由handleBarcodeSubmit处理)
      if (key === 'Enter') {
        if (this.scanCode.length > 0) {
          // é˜»æ­¢é»˜è®¤å›žè½¦è¡Œä¸ºï¼Œé¿å…è¡¨å•提交
          event.preventDefault();
          // æ‰«ç å®Œæˆï¼Œè‡ªåŠ¨è§¦å‘æŸ¥è¯¢
          this.isScanning = false;
          // æ ¹æ®å½“前扫码目标设置相应的输入框值
          if (this.scanTarget === 'tray') {
            this.trayBarcode = this.scanCode;
            this.handleTraySubmit();
          } else if (this.scanTarget === 'material') {
            this.barcode = this.scanCode;
            this.handleBarcodeSubmit();
          }
        }
        this.scanCode = '';
        this.lastKeyTime = null;
        return;
      }
      // æž„建扫码内容(快速连续输入视为扫码)
      if (this.lastKeyTime && currentTime - this.lastKeyTime < 50) {
        this.scanCode += key;
        this.isScanning = true;
      } else {
        this.scanCode = key;
        this.isScanning = true;
      }
      // è®¾ç½®è®¡æ—¶å™¨ï¼Œå¦‚果一段时间内没有输入,则重置扫描状态
      if (this.scanTimer) {
        clearTimeout(this.scanTimer);
      }
      this.scanTimer = setTimeout(() => {
        this.isScanning = false;
      }, 100);
      this.lastKeyTime = currentTime;
    },
    // åˆ é™¤ç‰©æ–™
    removeMaterial(index) {
      this.$confirm('确定要删除这条物料记录吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.materials.splice(index, 1);
        this.$message({
          type: 'success',
          message: '删除成功!'
        });
        this.fetchStockStatistics();
      }).catch(() => {
        // å–消删除
      });
    },
    // æ¸…空所有物料
    clearAllMaterials() {
      if (this.materials.length === 0) return;
      this.$confirm('确定要清空所有物料记录吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.materials = [];
        this.$message({
          type: 'success',
          message: '已清空所有记录!'
        });
      }).catch(() => {
        // å–消清空
      });
    },
    // æ ¼å¼åŒ–æ—¶é—´
    formatTime(date) {
      const year = date.getFullYear();
      const month = String(date.getMonth() + 1).padStart(2, '0');
      const day = String(date.getDate()).padStart(2, '0');
      const hours = String(date.getHours()).padStart(2, '0');
      const minutes = String(date.getMinutes()).padStart(2, '0');
      const seconds = String(date.getSeconds()).padStart(2, '0');
      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
    }
  }
}
</script>
<style scoped>
    .barcode-scanner-container {
      max-width: 1200px;
      margin: 0 auto;
      padding: 10px;
      display: flex;
      flex-direction: column;
      height: 100%;
      gap: 8px;
    }
    /* ç´§å‡‘布局样式 */
    .compact {
      margin-bottom: 0;
    }
    .compact-form {
      margin-bottom: 0;
    }
    .compact-item {
      margin-bottom: 0;
    }
    .compact-card {
      margin-bottom: 0;
    }
    .compact-card >>> .el-card__body {
      padding: 12px;
    }
    .compact-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 0 !important;
    }
    .compact-header >>> .el-card__header {
      padding: 8px 12px;
    }
    .compact-input {
      margin: 8px 0;
    }
    .compact-tips {
      margin-top: 8px;
      font-size: 11px;
    }
    /* ä»“库区域选择 - ç´§å‡‘ */
    .location-section.compact {
      margin-bottom: 8px;
    }
    .location-section.compact >>> .el-form-item {
      margin-bottom: 0;
    }
    /* æ‰˜ç›˜ä¿¡æ¯ - ç´§å‡‘ */
    .tray-info.compact {
      padding: 6px 10px;
      margin-bottom: 8px;
      font-size: 13px;
    }
    /* æ‰«ç åŒºåŸŸ - ç´§å‡‘ */
    .input-section.compact {
      margin-bottom: 8px;
      flex-shrink: 0;
    }
    /* ç‰©æ–™åˆ—表 - å›ºå®šé«˜åº¦å¸¦æ»šåЍ */
    .material-list.compact {
      flex: 1;
      min-height: 0; /* é‡è¦ï¼šå…è®¸flex子项收缩 */
      display: flex;
      flex-direction: column;
    }
    .material-list.compact >>> .el-card {
      display: flex;
      flex-direction: column;
      height: 100%;
    }
    .material-list.compact >>> .el-card__body {
      flex: 1;
      display: flex;
      flex-direction: column;
      padding: 0;
      min-height: 0;
    }
    .table-container {
      flex: 1;
      min-height: 0;
      overflow: hidden;
    }
    .material-list.compact >>> .el-table {
      flex: 1;
    }
    .material-list.compact >>> .el-table__body-wrapper {
      overflow-y: auto;
    }
    /* ç´§å‡‘的空状态 */
    .empty-state.compact {
      padding: 20px 0;
      flex: 1;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
    }
    .empty-state.compact i {
      font-size: 36px;
      margin-bottom: 8px;
    }
    .empty-state.compact p {
      font-size: 13px;
    }
    /* å…¶ä»–原有样式调整 */
    .page-title {
      text-align: center;
      margin-bottom: 15px;
    }
    .scan-status {
      font-size: 12px;
      color: #67C23A;
    }
    .scan-indicator {
      display: inline-block;
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background-color: #67C23A;
      margin-right: 5px;
      animation: pulse 1.5s infinite;
    }
    @keyframes pulse {
      0% { opacity: 1; }
      50% { opacity: 0.4; }
      100% { opacity: 1; }
    }
    .input-wrapper {
      position: relative;
    }
    .input-tips {
      margin-top: 6px;
      color: #909399;
    }
    .warning-text {
        color: #E6A23C;
        font-weight: bold;
    }
    .loading.compact {
      text-align: center;
      margin: 10px 0;
      padding: 5px;
    }
    .loading.compact p {
      margin-top: 5px;
      color: #409EFF;
      font-size: 12px;
    }
    .error-message.compact {
      margin: 5px 0;
    }
    .error-message.compact >>> .el-alert {
      padding: 6px 12px;
    }
    .list-actions {
      display: flex;
      align-items: center;
      gap: 4px;
    }
    .list-actions >>> .el-tag {
      height: 24px;
      line-height: 22px;
      padding: 0 6px;
    }
    .clear-all-btn {
      margin-left: 8px;
    }
    .material-code {
      font-family: 'Courier New', monospace;
      font-weight: bold;
      color: #409EFF;
    }
    .location-info {
        color: #606266;
        font-weight: normal;
    }
    .debug-info {
      background: #f5f7fa;
      padding: 8px;
      border-radius: 4px;
      margin-top: 8px;
      font-size: 11px;
      color: #909399;
    }
    .small-button {
      padding: 6px 8px;
      font-size: 11px;
    }
.barcode-scanner-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 10px;
  display: flex;
  flex-direction: column;
  height: 100%;
  gap: 8px;
}
    /* è¾“入框组样式调整 */
    .custom-input-group {
      display: flex;
      align-items: center;
      width: 100%;
      margin: 8px 0;
      border: 1px solid #DCDFE6;
      border-radius: 4px;
      overflow: hidden;
      background: #fff;
    }
/* ç´§å‡‘布局样式 */
.compact {
  margin-bottom: 0;
}
    .input-label {
      padding: 0 12px;
      background: #F5F7FA;
      border-right: 1px solid #DCDFE6;
      color: #606266;
      font-size: 13px;
      white-space: nowrap;
      height: 36px;
      line-height: 36px;
      flex-shrink: 0;
      min-width: 70px;
      text-align: center;
    }
.compact-form {
  margin-bottom: 0;
}
    .input-container {
      display: flex;
      flex: 1;
      align-items: center;
    }
.compact-item {
  margin-bottom: 0;
}
    .custom-input {
      flex: 1;
    }
.compact-card {
  margin-bottom: 0;
}
    .custom-input >>> .el-input__inner {
      border: none;
      border-radius: 0;
      height: 36px;
      line-height: 36px;
      font-size: 13px;
    }
    /* å“åº”式调整 */
    @media (max-width: 768px) {
      .barcode-scanner-container {
        padding: 5px;
      }
      .custom-input-group {
        flex-direction: column;
        border: none;
      }
      .input-label {
        width: 100%;
        border-right: none;
        border-bottom: 1px solid #DCDFE6;
        margin-bottom: 5px;
      }
      .input-container {
        width: 100%;
        border: 1px solid #DCDFE6;
        border-radius: 4px;
      }
    }
.compact-card>>>.el-card__body {
  padding: 12px;
}
.compact-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 !important;
}
.compact-header>>>.el-card__header {
  padding: 8px 12px;
}
.compact-input {
  margin: 8px 0;
}
.compact-tips {
  margin-top: 8px;
  font-size: 11px;
}
/* ä»“库区域选择 - ç´§å‡‘ */
.location-section.compact {
  margin-bottom: 8px;
}
.location-section.compact>>>.el-form-item {
  margin-bottom: 0;
}
/* æ‰˜ç›˜ä¿¡æ¯ - ç´§å‡‘ */
.tray-info.compact {
  padding: 6px 10px;
  margin-bottom: 8px;
  font-size: 13px;
}
/* æ‰«ç åŒºåŸŸ - ç´§å‡‘ */
.input-section.compact {
  margin-bottom: 8px;
  flex-shrink: 0;
}
/* ç‰©æ–™åˆ—表 - å›ºå®šé«˜åº¦å¸¦æ»šåЍ */
.material-list.compact {
  flex: 1;
  min-height: 0;
  /* é‡è¦ï¼šå…è®¸flex子项收缩 */
  display: flex;
  flex-direction: column;
}
.material-list.compact>>>.el-card {
  display: flex;
  flex-direction: column;
  height: 100%;
}
.material-list.compact>>>.el-card__body {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 0;
  min-height: 0;
}
.table-container {
  flex: 1;
  min-height: 0;
  overflow: hidden;
}
.material-list.compact>>>.el-table {
  flex: 1;
}
.material-list.compact>>>.el-table__body-wrapper {
  overflow-y: auto;
}
/* ç´§å‡‘的空状态 */
.empty-state.compact {
  padding: 20px 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
.empty-state.compact i {
  font-size: 36px;
  margin-bottom: 8px;
}
.empty-state.compact p {
  font-size: 13px;
}
/* å…¶ä»–原有样式调整 */
.page-title {
  text-align: center;
  margin-bottom: 15px;
}
.scan-status {
  font-size: 12px;
  color: #67C23A;
}
.scan-indicator {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background-color: #67C23A;
  margin-right: 5px;
  animation: pulse 1.5s infinite;
}
@keyframes pulse {
  0% {
    opacity: 1;
  }
  50% {
    opacity: 0.4;
  }
  100% {
    opacity: 1;
  }
}
.input-wrapper {
  position: relative;
}
.input-tips {
  margin-top: 6px;
  color: #909399;
}
.warning-text {
  color: #E6A23C;
  font-weight: bold;
}
.loading.compact {
  text-align: center;
  margin: 10px 0;
  padding: 5px;
}
.loading.compact p {
  margin-top: 5px;
  color: #409EFF;
  font-size: 12px;
}
.error-message.compact {
  margin: 5px 0;
}
.error-message.compact>>>.el-alert {
  padding: 6px 12px;
}
.list-actions {
  display: flex;
  align-items: center;
  gap: 4px;
}
.list-actions>>>.el-tag {
  height: 24px;
  line-height: 22px;
  padding: 0 6px;
}
.clear-all-btn {
  margin-left: 8px;
}
.material-code {
  font-family: 'Courier New', monospace;
  font-weight: bold;
  color: #409EFF;
}
.location-info {
  color: #606266;
  font-weight: normal;
}
.debug-info {
  background: #f5f7fa;
  padding: 8px;
  border-radius: 4px;
  margin-top: 8px;
  font-size: 11px;
  color: #909399;
}
.small-button {
  padding: 6px 8px;
  font-size: 11px;
}
/* è¾“入框组样式调整 */
.custom-input-group {
  display: flex;
  align-items: center;
  width: 100%;
  margin: 8px 0;
  border: 1px solid #DCDFE6;
  border-radius: 4px;
  overflow: hidden;
  background: #fff;
}
.input-label {
  padding: 0 12px;
  background: #F5F7FA;
  border-right: 1px solid #DCDFE6;
  color: #606266;
  font-size: 13px;
  white-space: nowrap;
  height: 36px;
  line-height: 36px;
  flex-shrink: 0;
  min-width: 70px;
  text-align: center;
}
.input-container {
  display: flex;
  flex: 1;
  align-items: center;
}
.custom-input {
  flex: 1;
}
.custom-input>>>.el-input__inner {
  border: none;
  border-radius: 0;
  height: 36px;
  line-height: 36px;
  font-size: 13px;
}
/* å“åº”式调整 */
@media (max-width: 768px) {
  .barcode-scanner-container {
    padding: 5px;
  }
  .custom-input-group {
    flex-direction: column;
    border: none;
  }
  .input-label {
    width: 100%;
    border-right: none;
    border-bottom: 1px solid #DCDFE6;
    margin-bottom: 5px;
  }
  .input-container {
    width: 100%;
    border: 1px solid #DCDFE6;
    border-radius: 4px;
  }
}
</style>
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/extension/inbound/extend/SelectedStock.vue
@@ -5,7 +5,7 @@
      :lazy="true"
      width="75%"
      :padding="15"
      title="出库详情"
      title="调拨出库详情"
    >
      <div class="box-head">
        <el-alert :closable="false" style="width: 100%">
@@ -182,14 +182,19 @@
          "查询中"
        )
        .then((x) => {
          var label=[
              { label: '已分配', value: 0 },
              { label: '出库中', value: 1 },
              { label: '出库完成', value: 2 },
              { label: '拣选完成', value: 3 },
              { label: '部分拣选', value: 2 },
              { label: '已拣选', value: 3 },
              { label: '已出库', value: 4 },
              { label: '出库完成', value: 5 },
              { label: '拣选完成', value: 6 },
              { label: '回库中', value: 7 },
              { label: '已回库', value: 8 },
              { label: '撤销', value: 99 }
          ]
          this.tableData=x.map((i) => ({
            ...i,
            status:label.find((j) => j.value === i.status).label
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/extension/inbound/extend/StockSelect.vue
@@ -132,11 +132,11 @@
  data() {
    return {
      row: null,
      showDetialBox: false,
      showDetialBox: false,
      tableData: [],
      tableColumns: [
        { prop: "materielCode", title: "物料编号", type: "string", width: 150 },
        { prop: "materielName", title: "物料名称", type: "string", width: 150 },
        { prop: "barcode", title: "物料条码", type: "string", width: 150 },
        { prop: "palletCode", title: "托盘编号", type: "string", width: 150 },
        { prop: "locationCode", title: "货位编号", type: "string", width: 180 },
        { prop: "useableQuantity", title: "可用数量", type: "string" },
@@ -201,30 +201,23 @@
        if (!valid) return;
        // æž„造请求参数
        const keys = this.selection.map((item) => item.id);
        const requestParams = {
          taskIds: keys,
          outboundPlatform: this.outboundForm.selectedPlatform,
        };
          console.log(this.selection)
        // è°ƒç”¨å‡ºåº“接口
        this.http
          .post("api/Task/GenerateOutboundTasks", requestParams, "数据处理中")
          .then((x) => {
            if (!x.status) return ElMessage.error(x.message);
            ElMessage.success("操作成功");
            this.showOutboundDialog = false;
            this.showDetialBox = false;
            this.$emit("parentCall", ($vue) => {
              $vue.getData();
            });
          })
          .catch((error) => {
            console.error("出库请求失败:", error);
            ElMessage.error("请求失败,请稍后重试");
     if (this.selection.length <= 0) {
        return this.$message.error("请勾选");
      }
      let url = this.pkcx
        ? "api/Task/GenerateOutboundTask?orderDetailId="
        : "api/Task/GenerateOutboundTask?orderDetailId=";
      this.http
        .post(url + this.row.id, this.selection, "数据处理中")
        .then((x) => {
          if (!x.status) return this.$message.error(x.message);
          this.$message.success("操作成功");
          this.showDetialBox = false;
          this.$emit("parentCall", ($vue) => {
            $vue.getData();
          });
        });
      });
    },
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/extension/inbound/extend/allocateOrderDetail.vue
@@ -28,13 +28,13 @@
                @click="handleOpenPicking"
                >拣选</el-link>
                 
              <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"
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/extension/inbound/inboundOrder.js
@@ -71,9 +71,8 @@
                        // å‘起撤销组盘请求
                        try {
                            //console.log('发起撤销组盘请求,托盘号:', formData.palletCode.trim());
                            const response = await http.post('/api/InboundOrder/CancelPalletGroup', {
                                palletCode: formData.palletCode.trim()
                            });
                            const response = await http.post('/api/InboundOrder/UndoPalletGroup?palletCode='+formData.palletCode.trim());
                            const { status, message, data } = response;
                            if (status) {
@@ -416,7 +415,14 @@
      searchBefore(param) {
        //界面查询前,可以给param.wheres添加查询参数
        //返回false,则不会执行查询
       this.searchFormFields.orderType=[0]
      // this.searchFormFields.orderType=[0]
         let wheres = [{
            'name': 'orderType',
            'value': '0',
            'displayType': 'text'}];
          param.wheres.push(...wheres);
        return true;
      },
      searchAfter(result) {
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/extension/outbound/extend/SelectedStock.vue
@@ -111,6 +111,12 @@
          width: 90,
        },
        {
          prop: "currentBarcode",
          title: "条码",
          type: "string",
          width: 90,
        },
        {
          prop: "batchNo",
          title: "批次号",
          type: "string",
@@ -184,12 +190,17 @@
        .then((x) => {
          
          var label=[
              { label: '已分配', value: 0 },
             { label: '已分配', value: 0 },
              { label: '出库中', value: 1 },
              { label: '出库完成', value: 2 },
              { label: '拣选完成', value: 3 },
              { label: '部分拣选', value: 2 },
              { label: '已拣选', value: 3 },
              { label: '已出库', value: 4 },
              { label: '出库完成', value: 5 },
              { label: '拣选完成', value: 6 },
              { label: '回库中', value: 7 },
              { label: '已回库', value: 8 },
              { label: '撤销', value: 99 }
          ]
          ];
          this.tableData=x.map((i) => ({
            ...i,
            status:label.find((j) => j.value === i.status).label
ÏîÄ¿´úÂë/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() {
@@ -200,6 +213,7 @@
          title: "指定库存",
          type: "icon",
          width: 90,
          hidden:true,
          icon: "el-icon-s-grid",
        },
        {
@@ -336,6 +350,11 @@
        path: '/outbound/picking',
        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) {
@@ -474,6 +493,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;
            }
console.log(this.selection);
            // 4. æž„造请求参数(新增小数字段)
            const keys = this.selection.map((item) => item.id);
            const requestParams = {
              orderDetailId: keys[0], // åˆ†æ‰¹å‡ºåº“仅支持单条明细
              outboundPlatform: formData.selectedPlatform, // å‡ºåº“站台
              batchQuantity: formData.outboundDecimal // æ–°å¢žï¼šå°æ•°å­—段传给后端
            };
            // 5. è°ƒç”¨å‡ºåº“接口
            this.http
              .post("api/Task/GenerateOutboundBatchTasks", 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/extension/outbound/extend/printView.vue
@@ -172,7 +172,7 @@
  },
  methods: {
    generateQr(item) {
      return `${item.materialCode}_${item.supplierCode}_${item.purchaseOrderNo}_${item.materialName}_${item.batch}_${item.batchNumber}_${item.factory}_${item.date}`;
      return `${item.batchNumber}`;
    },
    open(rows) {
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/extension/outbound/outboundOrder.js
@@ -327,15 +327,6 @@
          this.$refs.gridBody.open(row);
        }
      });
      let TaskHandCompletedBtn = this.buttons.find(x => x.value == 'NoStockOut');
      if (TaskHandCompletedBtn) {
        TaskHandCompletedBtn.onClick = function () {
          this.$refs.gridHeader.open();
        }
      }
    },
    onInited() {
      //框架初始化配置后
@@ -345,6 +336,16 @@
    searchBefore(param) {
      //界面查询前,可以给param.wheres添加查询参数
      //返回false,则不会执行查询
           let wheres = [{
            'name': 'orderType',
            'value': '0',
            'displayType': 'text'}];
          param.wheres.push(...wheres);
        return true;
      return true;
    },
    searchAfter(result) {
ÏîÄ¿´úÂë/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')
@@ -197,7 +204,7 @@
    component: () => import('@/views/inbound/allocateinboundOrder.vue')
  },{
    path:'allocateoutboundOrder',
    path:'/allocateoutboundOrder',
    name: 'allocateoutboundOrder',
    component: () => import('@/views/outbound/allocateoutboundOrder.vue') 
  },
@@ -205,6 +212,11 @@
    path: '/allocateOrder',
    name: 'Dt_AllocateOrder',
    component: () => import('@/views/inbound/Dt_AllocateOrder.vue')
  },
  {
    path: '/reCheckOrder',
    name: 'Dt_ReCheckOrder',
    component: () => import('@/views/check/ReCheckOrder.vue')
  }
]
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/check/ReCheckOrder.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,115 @@
<!--
*Author:jxx
 *Contact:283591387@qq.com
 *代码由框架生成,任何更改都可能导致被代码生成器覆盖
 *业务请在@/extension/widesea_wcs/order/Dt_CheckOrder.js此处编写
 -->
<template>
    <view-grid ref="grid" :columns="columns" :editFormFields="editFormFields"
        :editFormOptions="editFormOptions" :searchFormFields="searchFormFields" :searchFormOptions="searchFormOptions"
        :table="table" :extend="extend">
    </view-grid>
</template>
<script>
import extend from "@/extension/check/recheckOrder.js";
import { ref, defineComponent } from "vue";
export default defineComponent({
    setup() {
        const table = ref({
            key: 'id',
            footer: "Foots",
            cnName: '重检单',
            name: 'Dt_ReCheckOrder',
            url: "/ReCheckOrder/",
            sortName: "id"
        });
        const editFormFields = ref({
            OrderNo: "",
            MaterielCode: "",
            BatchNo: "",
            WarehouseCode: "",
            Unit: "",
            FactoryArea: "",
            Result: 0,
            InspectionNumber: 0,
            Qty: 0,
            SignSeq: 0
        });
        const editFormOptions = ref([
            [
                { title: "复检单号", field: "OrderNo", type: "input", require: true },
                { title: "物料编号", field: "MaterielCode", type: "input", require: true },
                { title: "批次号", field: "BatchNo", type: "input", require: true },
                { title: "仓库编码", field: "WarehouseCode", type: "input", require: true }
            ],
            [
                { title: "单位", field: "Unit", type: "input", require: true },
                { title: "厂区", field: "FactoryArea", type: "input", require: true },
                { title: "检验结果", field: "Result", type: "select", require: true,dataKey: "inOrderType",data: []},
                { title: "检验次数", field: "InspectionNumber", type: "number", require: true, min: 1 }
            ],
            [
                { title: "数量", field: "Qty", type: "number", require: true, min: 0 },
                { title: "签字顺序", field: "SignSeq", type: "number", require: true, min: 1 }
            ]
        ]);
        const searchFormFields = ref({
            OrderNo: "",
            MaterielCode: "",
            BatchNo: "",
            WarehouseCode: "",
            FactoryArea: "",
            Result: ""
        });
        const searchFormOptions = ref([
            [
                { title: "复检单号", field: "OrderNo", type: "like" },
                { title: "物料编号", field: "MaterielCode", type: "like" },
                { title: "批次号", field: "BatchNo", type: "like" },
                { title: "仓库编码", field: "WarehouseCode", type: "like" }
            ],
            [
                { title: "厂区", field: "FactoryArea", type: "like" },
                { title: "检验结果", field: "Result", type: "select",dataKey: "inOrderType", data: []}
            ]
        ]);
        const columns = ref([
            { field: 'id', title: '主键', type: 'int', width: 150, hidden: true, readonly: true, require: true, align: 'left' },
            { field: 'orderNo', title: '复检单号', type: 'string', width: 160, require: true, align: 'left', sort: true },
            { field: 'materielCode', title: '物料编号', type: 'string', width: 160, require: true, align: 'left' },
            { field: 'batchNo', title: '批次号', type: 'string', width: 160, require: true, align: 'left' },
            { field: 'warehouseCode', title: '仓库编码', type: 'string', width: 160, require: true, align: 'left' },
            { field: 'unit', title: '单位', type: 'string', width: 100, require: true, align: 'left' },
            { field: 'factoryArea', title: '厂区', type: 'string', width: 120, require: true, align: 'left' },
            { field: 'result', title: '检验结果', type: 'int', width: 110, require: true, align: 'left',bind: { key: "inOrderType", data: [] },},
            { field: 'inspectionNumber', title: '检验次数', type: 'int', width: 110, require: true, align: 'left' },
            { field: 'qty', title: '数量', type: 'float', width: 110, require: true, align: 'left' },
            { field: 'signSeq', title: '签字顺序', type: 'int', width: 110, require: true, align: 'left' },
            { field: 'creater', title: '创建者', type: 'string', width: 110, require: true, align: 'left' },
            { field: 'createDate', title: '创建时间', type: 'datetime', width: 150, require: true, align: 'left', sort: true },
            { field: 'modifier', title: '修改人', type: 'string', width: 100, align: 'left' },
            { field: 'modifyDate', title: '修改日期', type: 'datetime', width: 150, align: 'left', sort: true },
        ]);
        return {
            table,
            extend,
            editFormFields,
            editFormOptions,
            searchFormFields,
            searchFormOptions,
            columns
        };
    },
});
</script>
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/check/checkOrder.vue
@@ -5,98 +5,92 @@
 *业务请在@/extension/widesea_wcs/order/Dt_CheckOrder.js此处编写
 -->
<template>
    <view-grid ref="grid"
               :columns="columns"
               :detail="detail"
               :editFormFields="editFormFields"
               :editFormOptions="editFormOptions"
               :searchFormFields="searchFormFields"
               :searchFormOptions="searchFormOptions"
               :table="table"
               :extend="extend">
    <view-grid ref="grid" :columns="columns" :detail="detail" :editFormFields="editFormFields"
        :editFormOptions="editFormOptions" :searchFormFields="searchFormFields" :searchFormOptions="searchFormOptions"
        :table="table" :extend="extend">
    </view-grid>
</template>
<script>
    import extend from "@/extension/check/checkOrder.js";
    import { ref, defineComponent } from "vue";
    export default defineComponent({
        setup() {
            const table = ref({
                key: 'checkOrderId',
                footer: "Foots",
                cnName: '检验单',
                name: 'checkOrder',
                url: "/CheckOrder/",
                sortName: "checkOrderId"
            });
            const editFormFields = ref({});
            const editFormOptions = ref([]);
            const searchFormFields = ref({
                checkOrderNo:"",
                receiveOrderNo:"",
                checkOrderStatus:"",
                auditStatus:""
            });
            const searchFormOptions = ref([
                [
                    {title:"检验单号", field:"checkOrderNo",type:"like"},
                    {title:"收货单号", field:"receiveOrderNo",type:"like"},
                    {title:"质检单状态", field:"checkOrderStatus",type:"select",dataKey:"",data:[]},
                    {title:"审批状态", field:"auditStatus",type:"select",dataKey:"",data:[]},
                ],
                [
                    {title:"收货单明细行号", field:"receiveDetailRowNo",type:"like"},
                    {title:"物料编号", field:"materielCode",type:"like"},
                    {title:"检验结果", field:"result",type:"like"},
                ]
            ]);
            const columns = ref([{field:'checkOrderId',title:'主键',type:'int',width:150,hidden:true,readonly:true,require:true,align:'left'},
                       {field:'checkOrderNo',title:'检验单号',type:'string',width:160,require:true,align:'left',sort:true},
                       {field:'receiveOrderNo',title:'收货单号',type:'string',width:160,require:true,align:'left'},
                       {field:'checkOrderStatus',title:'质检单状态',type:'int',width:110,require:true,align:'left'},
                       {field:'auditStatus',title:'审批状态',type:'int',width:110,require:true,align:'left'},
                       {field:'receiveDetailRowNo',title:'收货单明细行号',type:'int',width:110,require:true,align:'left'},
                       {field:'materielCode',title:'物料编号',type:'string',width:110,require:true,align:'left'},
                       {field:'qualifiedQuantity',title:'合格数量',type:'float',width:110,align:'left'},
                       {field:'defectedQuantity',title:'特采数量',type:'float',width:110,align:'left'},
                       {field:'returnQuantity',title:'退货数量',type:'float',width:110,align:'left'},
                       {field:'scrappedQuantity',title:'报废数量',type:'float',width:110,align:'left'},
                       {field:'receivedQuantity',title:'检验总数',type:'float',width:110,require:true,align:'left'},
                       {field:'result',title:'检验结果',type:'string',width:110,align:'left'},
                       {field:'defectedNote',title:'特采说明',type:'string',width:110,align:'left'},
                       {field:'checkUserName',title:'检验人',type:'string',width:110,align:'left'},
                       {field:'creater',title:'创建者',type:'string',width:110,require:true,align:'left'},
                       {field:'createDate',title:'创建时间',type:'datetime',width:150,require:true,align:'left',sort:true},
                       {field:'modifier',title:'修改人',type:'string',width:100,align:'left'},
                       {field:'modifyDate',title:'修改日期',type:'datetime',width:150,align:'left',sort:true}]);
            const detail = ref({
                cnName: "检验结果",
                table: "CheckOrderResult",
                columns: [
                    {field:'id',title:'主键',type:'int',width:110,hidden:true,readonly:true,require:true,align:'left'},
                    {field:'checkOrderId',title:'检验单主键',type:'int',width:110,require:true,align:'left',sort:true},
                    {field:'defectCode',title:'缺陷代码',type:'string',width:110,require:true,align:'left'},
                    {field:'result',title:'检验结果',type:'string',width:110,require:true,align:'left'},
                    {field:'quantity',title:'数量',type:'float',width:110,require:true,align:'left'},
                    {field:'note',title:'备注',type:'string',width:220,align:'left'},
                    {field:'creater',title:'创建者',type:'string',width:110,require:true,align:'left'},
                    {field:'createDate',title:'创建时间',type:'datetime',width:150,require:true,align:'left',sort:true},
                    {field:'modifier',title:'修改人',type:'string',width:100,align:'left'},
                    {field:'modifyDate',title:'修改日期',type:'datetime',width:150,align:'left',sort:true}
                ],
                sortName: "id",
                key: "id"
            });
            return {
                table,
                extend,
                editFormFields,
                editFormOptions,
                searchFormFields,
                searchFormOptions,
                columns,
                detail,
            };
        },
    });
import extend from "@/extension/check/checkOrder.js";
import { ref, defineComponent } from "vue";
export default defineComponent({
    setup() {
        const table = ref({
            key: 'checkOrderId',
            footer: "Foots",
            cnName: '检验单',
            name: 'checkOrder',
            url: "/CheckOrder/",
            sortName: "checkOrderId"
        });
        const editFormFields = ref({});
        const editFormOptions = ref([]);
        const searchFormFields = ref({
            checkOrderNo: "",
            receiveOrderNo: "",
            checkOrderStatus: "",
            auditStatus: ""
        });
        const searchFormOptions = ref([
            [
                { title: "检验单号", field: "checkOrderNo", type: "like" },
                { title: "收货单号", field: "receiveOrderNo", type: "like" },
                { title: "质检单状态", field: "checkOrderStatus", type: "select", dataKey: "", data: [] },
                { title: "审批状态", field: "auditStatus", type: "select", dataKey: "", data: [] },
            ],
            [
                { title: "收货单明细行号", field: "receiveDetailRowNo", type: "like" },
                { title: "物料编号", field: "materielCode", type: "like" },
                { title: "检验结果", field: "result", type: "like" },
            ]
        ]);
        const columns = ref([{ field: 'checkOrderId', title: '主键', type: 'int', width: 150, hidden: true, readonly: true, require: true, align: 'left' },
        { field: 'checkOrderNo', title: '检验单号', type: 'string', width: 160, require: true, align: 'left', sort: true },
        { field: 'receiveOrderNo', title: '收货单号', type: 'string', width: 160, require: true, align: 'left' },
        { field: 'checkOrderStatus', title: '质检单状态', type: 'int', width: 110, require: true, align: 'left' },
        { field: 'auditStatus', title: '审批状态', type: 'int', width: 110, require: true, align: 'left' },
        { field: 'receiveDetailRowNo', title: '收货单明细行号', type: 'int', width: 110, require: true, align: 'left' },
        { field: 'materielCode', title: '物料编号', type: 'string', width: 110, require: true, align: 'left' },
        { field: 'qualifiedQuantity', title: '合格数量', type: 'float', width: 110, align: 'left' },
        { field: 'defectedQuantity', title: '特采数量', type: 'float', width: 110, align: 'left' },
        { field: 'returnQuantity', title: '退货数量', type: 'float', width: 110, align: 'left' },
        { field: 'scrappedQuantity', title: '报废数量', type: 'float', width: 110, align: 'left' },
        { field: 'receivedQuantity', title: '检验总数', type: 'float', width: 110, require: true, align: 'left' },
        { field: 'result', title: '检验结果', type: 'string', width: 110, align: 'left' },
        { field: 'defectedNote', title: '特采说明', type: 'string', width: 110, align: 'left' },
        { field: 'checkUserName', title: '检验人', type: 'string', width: 110, align: 'left' },
        { field: 'creater', title: '创建者', type: 'string', width: 110, require: true, align: 'left' },
        { field: 'createDate', title: '创建时间', type: 'datetime', width: 150, require: true, align: 'left', sort: true },
        { field: 'modifier', title: '修改人', type: 'string', width: 100, align: 'left' },
        { field: 'modifyDate', title: '修改日期', type: 'datetime', width: 150, align: 'left', sort: true }]);
        const detail = ref({
            cnName: "检验结果",
            table: "CheckOrderResult",
            columns: [
                { field: 'id', title: '主键', type: 'int', width: 110, hidden: true, readonly: true, require: true, align: 'left' },
                { field: 'checkOrderId', title: '检验单主键', type: 'int', width: 110, require: true, align: 'left', sort: true },
                { field: 'defectCode', title: '缺陷代码', type: 'string', width: 110, require: true, align: 'left' },
                { field: 'result', title: '检验结果', type: 'string', width: 110, require: true, align: 'left' },
                { field: 'quantity', title: '数量', type: 'float', width: 110, require: true, align: 'left' },
                { field: 'note', title: '备注', type: 'string', width: 220, align: 'left' },
                { field: 'creater', title: '创建者', type: 'string', width: 110, require: true, align: 'left' },
                { field: 'createDate', title: '创建时间', type: 'datetime', width: 150, require: true, align: 'left', sort: true },
                { field: 'modifier', title: '修改人', type: 'string', width: 100, align: 'left' },
                { field: 'modifyDate', title: '修改日期', type: 'datetime', width: 150, align: 'left', sort: true }
            ],
            sortName: "id",
            key: "id"
        });
        return {
            table,
            extend,
            editFormFields,
            editFormOptions,
            searchFormFields,
            searchFormOptions,
            columns,
            detail,
        };
    },
});
</script>
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/inbound/Dt_AllocateOrder.vue
@@ -37,7 +37,7 @@
      key: "id",
      footer: "Foots",
      cnName: "调拨单",
      name: "Dt_AllocateOrder",
      name: "AllocateOrder",
      url: "/AllocateOrder/",
      sortName: "id",
    });
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/inbound/allocateinboundOrder.vue
@@ -37,14 +37,14 @@
    const table = ref({
      key: "id",
      footer: "Foots",
      cnName: "调拨单(智仓调外部仓库)",
      name: "inboundOrder",
      url: "/InboundOrder/",
      cnName: "调拨单(外部仓调入智仓)",
      name: "AllocateOrder",
      url: "/AllocateOrder/",
      sortName: "id",
    });
    const editFormFields = ref({
      orderType: "",
      inboundOrderNo: "",
      orderNo: "",
      upperOrderNo: "",
      remark: "",
    });
@@ -59,7 +59,7 @@
          data: [],
        },
        {
          field: "inboundOrderNo",
          field: "orderNo",
          title: "单据编号",
          type: "string",
        },
@@ -76,7 +76,7 @@
      ],
    ]);
    const searchFormFields = ref({
      inboundOrderNo: "",
      orderNo: "",
      upperOrderNo: "",
      orderType: "115",
      orderStatus: "",
@@ -86,7 +86,7 @@
    });
    const searchFormOptions = ref([
      [
        { title: "单据编号", field: "inboundOrderNo", type: "like" },
        { title: "单据编号", field: "orderNo", type: "like" },
        { title: "上游单据编号", field: "upperOrderNo", type: "like" },
        {
          title: "单据类型",
@@ -127,7 +127,7 @@
        align: "left",
      },
      {
        field: "inboundOrderNo",
        field: "orderNo",
        title: "单据编号",
        type: "string",
        width: 120,
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/inbound/inboundOrder.vue
@@ -57,6 +57,7 @@
          type: "select",
          dataKey: "inOrderType",
          data: [],
          hidden:true
        },
        {
          field: "inboundOrderNo",
@@ -94,6 +95,7 @@
          type: "select",
          dataKey: "inOrderType",
          data: [0],
          hidden:true
        },
        {
          title: "单据状态",
@@ -148,6 +150,7 @@
        width: 150,
        align: "left",
        bind: { key: "inOrderType", data: [] },
        hidden:true
      },
      {
        field: "businessType",
@@ -242,7 +245,7 @@
          field: "materielCode",
          title: "物料编号",
          type: "select",
          width: 150,
          width: 120,
          align: "left",
          edit: { type: "" },
          required: true,
@@ -251,7 +254,7 @@
          field: "materielCode",
          title: "物料名称",
          type: "string",
          width: 100,
          width: 120,
          align: "left",
          bind: { key: "MaterielNames", data: [] },
        },
@@ -259,7 +262,7 @@
          field: "batchNo",
          title: "批次号",
          type: "decimal",
          width: 90,
          width: 150,
          align: "left",
          edit: { type: "" },
          required: true,
@@ -268,7 +271,7 @@
          field: "supplyCode",
          title: "供应商编号",
          type: "decimal",
          width: 90,
          width: 100,
          align: "left",
          edit: { type: "" },
          required: true,
@@ -285,7 +288,7 @@
          field: "barcode",
          title: "条码",
          type: "decimal",
          width: 90,
          width: 180,
          align: "left",
          edit: { type: "" },
          required: true,
@@ -303,21 +306,21 @@
          field: "receiptQuantity",
          title: "组盘数量",
          type: "int",
          width: 120,
          width: 90,
          align: "left",
        },
        {
          field: "overInQuantity",
          title: "上架数量",
          type: "string",
          width: 200,
          width: 90,
          align: "left",
        },
        {
          field: "orderDetailStatus",
          title: "订单明细状态",
          type: "string",
          width: 180,
          width: 100,
          align: "left",
          bind: { key: "orderDetailStatusEnum", data: [] },
        },
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/outbound/BatchPickingConfirm.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1278 @@
<template>
  <div class="OutboundPicking-container">
    <div class="page-header">
      <el-page-header @back="goBack">
        <template #content>
          <span class="title">出库拣选确认 - {{ this.$route.query.orderNo }}</span>
        </template>
      </el-page-header>
    </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="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="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.palletCode" disabled></el-input>
              </el-form-item>
              <el-form-item label="原条码" prop="originalBarcode">
                <div style="display: flex; align-items: center; gap: 10px;">
                  <el-input
                    v-model="splitForm.originalBarcode"
                    placeholder="扫描原条码"
                    @keyup.enter.native="onSplitBarcodeScan"
                    @change="onSplitBarcodeScan"
                    clearable
                    style="flex: 1;">
                  </el-input>
                  <!-- æ–°å¢žï¼šæŸ¥çœ‹æ‹†åŒ…链按钮 -->
                  <el-button
  type="primary"
  @click="viewSplitChainFromSplit(splitForm.originalBarcode)"
  :disabled="!splitForm.originalBarcode"
  :loading="splitChainLoading">
  æŸ¥çœ‹æ‹†åŒ…链
</el-button>
                </div>
              </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">
                <div style="display: flex; align-items: center; gap: 10px;">
                  <el-input
                    v-model="revertSplitForm.newBarcode"
                    placeholder="扫描新条码"
                    @keyup.enter.native="onRevertSplitBarcodeScan"
                    @change="onRevertSplitBarcodeScan"
                    clearable
                    style="flex: 1;">
                  </el-input>
                  <!-- æ–°å¢žï¼šæŸ¥çœ‹æ‹†åŒ…链按钮 -->
                  <el-button
                    type="primary"
                    @click="viewSplitChain(revertSplitForm.newBarcode)"
                    :disabled="!revertSplitForm.newBarcode">
                    æŸ¥çœ‹æ‹†åŒ…链
                  </el-button>
                </div>
              </el-form-item>
            </el-form>
            <!-- æ–°å¢žï¼šæ‹†åŒ…链简要信息显示 -->
            <div v-if="splitChainInfo.splitChain && splitChainInfo.splitChain.length > 0"
                 style="margin-top: 15px; padding: 10px; background: #f0f9ff; border-radius: 4px;">
              <div style="font-size: 14px; color: #606266;">
                <div>拆包链信息: å…± {{ splitChainInfo.totalSplitTimes }} æ¬¡æ‹†åŒ…</div>
                <div style="margin-top: 5px;">
                  <el-tag
                    v-for="item in splitChainInfo.splitChain.slice(0, 3)"
                    :key="item.newBarcode"
                    :type="item.isReverted ? 'success' : 'primary'"
                    size="small"
                    style="margin-right: 5px;">
                    {{ item.newBarcode }} ({{ item.splitQuantity }})
                  </el-tag>
                  <span v-if="splitChainInfo.splitChain.length > 3" style="color: #909399;">
                    ç­‰ {{ splitChainInfo.splitChain.length }} ä¸ªæ¡ç 
                  </span>
                </div>
              </div>
            </div>
          </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="showSplitChainDialog" class="custom-dialog-overlay">
  <div class="custom-dialog-wrapper">
    <div class="custom-dialog" style="width: 750px;">
      <div class="custom-dialog-header">
        <h3>拆包链信息</h3>
        <el-button type="text" @click="closeSplitChainDialog" class="close-button">×</el-button>
      </div>
      <div class="custom-dialog-body">
        <!-- æ–°å¢žï¼šæ‹†åŒ…链说明 -->
        <div style="margin-bottom: 15px; padding: 10px; background: #f0f9ff; border-radius: 4px;">
          <div style="display: flex; justify-content: space-between; align-items: center;">
            <div>
              <div style="font-weight: bold; color: #303133;">拆包链说明</div>
              <div style="font-size: 12px; color: #606266; margin-top: 5px;">
                å½“前显示的是从 <el-tag type="primary" size="small">{{ splitChainInfo.originalBarcode }}</el-tag> å¼€å§‹çš„æ‹†åŒ…链
                <br>共 {{ splitChainInfo.totalSplitTimes }} æ¬¡æ‹†åŒ…操作,涉及 {{ splitChainInfo.splitChain.length }} ä¸ªæ¡ç 
              </div>
            </div>
            <el-button
              type="primary"
              size="small"
              @click="findRootChain(splitChainInfo.originalBarcode)"
              v-if="splitChainInfo.chainType !== 'root'">
              æŸ¥æ‰¾å®Œæ•´æ‹†åŒ…链
            </el-button>
          </div>
        </div>
        <div style="margin-bottom: 15px;">
          <el-tag type="info">总拆包次数: {{ splitChainInfo.totalSplitTimes }}</el-tag>
          <el-tag type="warning" style="margin-left: 10px;">
            åŽŸå§‹æ¡ç : {{ splitChainInfo.originalBarcode }}
          </el-tag>
          <el-tag :type="splitChainInfo.chainType === 'root' ? 'success' : 'warning'" style="margin-left: 10px;">
            {{ splitChainInfo.chainType === 'root' ? '完整链' : '分支链' }}
          </el-tag>
        </div>
        <el-table :data="splitChainInfo.splitChain" border height="300">
          <el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
          <el-table-column prop="splitTime" label="拆包时间" width="160">
            <template #default="scope">
              {{ formatDateTime(scope.row.splitTime) }}
            </template>
          </el-table-column>
          <el-table-column prop="originalBarcode" label="原条码" width="140">
            <template #default="scope">
              <el-tag
                :type="scope.row.originalBarcode === splitChainInfo.rootBarcode ? 'success' : 'primary'"
                size="small">
                {{ scope.row.originalBarcode }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="newBarcode" label="新条码" width="140">
            <template #default="scope">
              <el-tag
                :type="scope.row.isReverted ? 'info' : 'warning'"
                size="small">
                {{ scope.row.newBarcode }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="splitQuantity" label="拆包数量" width="100" align="right">
            <template #default="scope">
              {{ scope.row.splitQuantity.toFixed(2) }}
            </template>
          </el-table-column>
          <el-table-column prop="operator" label="操作员" width="100"></el-table-column>
          <el-table-column prop="isReverted" label="状态" width="80" align="center">
            <template #default="scope">
              <el-tag
                :type="scope.row.isReverted ? 'success' : 'danger'"
                size="small">
                {{ scope.row.isReverted ? '已撤销' : '有效' }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="操作" width="120" align="center">
            <template #default="scope">
              <el-button
                v-if="!scope.row.isReverted"
                type="danger"
                size="mini"
                @click="cancelSingleSplit(scope.row.newBarcode)"
                :disabled="hasPicked(scope.row.newBarcode)">
                å–消
              </el-button>
              <span v-else style="color: #909399;">已撤销</span>
            </template>
          </el-table-column>
        </el-table>
        <!-- æ‰¹é‡æ“ä½œåŒºåŸŸ -->
        <div style="margin-top: 15px; padding: 10px; background: #f5f7fa; border-radius: 4px;">
          <div style="display: flex; justify-content: space-between; align-items: center;">
            <div>
              <span style="font-size: 14px; color: #606266;">
                æ‰¹é‡æ“ä½œ: å¯ä»¥å–消整个拆包链或选择单个拆包记录取消
              </span>
              <div style="font-size: 12px; color: #909399; margin-top: 5px;">
                å®Œæ•´æ‹†åŒ…链包含从最原始条码开始的所有拆包操作
              </div>
            </div>
            <div>
              <el-button
                type="danger"
                @click="cancelWholeSplitChain"
                :disabled="!canCancelWholeChain"
                :loading="revertSplitLoading">
                å–消整个拆包链
              </el-button>
            </div>
          </div>
        </div>
      </div>
      <div class="custom-dialog-footer">
        <el-button @click="closeSplitChainDialog">关闭</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.palletCode" 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() {
    return {
      scanData: {
        orderNo: '',
        palletCode: '',
        barcode: ''
      },
      unpickedList: [],
      pickedList: [],
      selectedPickedRows: [],
      summary: {
        unpickedCount: 0,
        unpickedQuantity: 0,
        pickedCount: 0
      },
      palletStatus: '未知',
      // å¼¹çª—状态
      showCustomSplitDialog: false,
      showRevertSplitDialog: false,
      showBatchReturnDialog: false,
      showEmptyPalletDialog: false,
      showSplitChainDialog: false, // æ–°å¢žï¼šæ‹†åŒ…链信息弹窗
      // åŠ è½½çŠ¶æ€
      splitLoading: false,
      revertSplitLoading: false,
      batchReturnLoading: false,
      emptypalletOutLoading: false,
      splitChainLoading: false, // æ–°å¢žï¼šæ‹†åŒ…链加载状态
      // è¡¨å•数据
      splitForm: {
        orderNo: '',
        palletCode: '',
        originalBarcode: '',
        materielCode: '',
        splitQuantity: 0,
        maxQuantity: 0
      },
      revertSplitForm: {
        newBarcode: ''
      },
      batchReturnForm: {
        orderNo: '',
        palletCode: '',
        unpickedCount: 0,
        unpickedQuantity: 0
      },
      emptypalletOutForm: {
        orderNo: '',
        palletCode: ''
      },
      // æ–°å¢žï¼šæ‹†åŒ…链相关数据
      splitChainInfo: {
        originalBarcode: '',
        totalSplitTimes: 0,
        splitChain: []
      },
      // éªŒè¯è§„则
      splitFormRules: {
        originalBarcode: [
          { required: true, message: '请输入原条码', trigger: 'blur' }
        ],
        splitQuantity: [
          { required: true, message: '请输入拆包数量', trigger: 'blur' },
          { type: 'number', min: 0.01, message: '拆包数量必须大于0', trigger: 'blur' }
        ]
      },
      revertSplitFormRules: {
        newBarcode: [
          { required: true, message: '请输入新条码', trigger: 'blur' }
        ]
      },
      emptypalletOutFormRules: {
        palletCode: [
          { required: true, message: '请输入托盘码', trigger: 'blur' }
        ]
      },
      isProcessing: false
    }
  },
  computed: {
    // æ˜¯å¦å¯ä»¥å–消整个拆包链
    canCancelWholeChain() {
      return this.splitChainInfo.splitChain &&
             this.splitChainInfo.splitChain.some(item => !item.isReverted);
    }
  },
  mounted() {
    if (this.$route.query.orderNo) {
      this.scanData.orderNo = this.$route.query.orderNo;
      this.splitForm.orderNo = this.$route.query.orderNo;
      this.batchReturnForm.orderNo = this.$route.query.orderNo;
      this.emptypalletOutForm.orderNo = this.$route.query.orderNo;
    }
    this.$nextTick(() => {
      this.$refs.palletInput.focus();
    });
  },
  methods: {
    goBack(){
      this.$router.back()
    },
    // åˆ†æ‹£ç›¸å…³æ–¹æ³•
    async confirmPicking() {
      if (this.isProcessing) return;
      if (!this.scanData.orderNo || !this.scanData.palletCode || !this.scanData.barcode) {
        this.$message.warning('请先扫描托盘码和物料条码');
        this.focusBarcodeInput();
        return;
      }
      this.isProcessing = true;
      try {
        const res = await http.post('/api/OutboundBatchPicking/confirm-picking', {
          orderNo: this.scanData.orderNo,
          palletCode: this.scanData.palletCode,
          barcode: this.scanData.barcode
        });
        if (res.status) {
          this.$message.success('拣选确认成功');
          this.scanData.barcode = '';
          await this.loadPalletData();
          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;
      }
      this.showCustomSplitDialog = true;
      this.resetSplitForm();
      this.splitForm.orderNo = this.scanData.orderNo;
      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/OutboundBatchPicking/split-package-info', {
          orderNo: this.splitForm.orderNo,
          palletCode: this.splitForm.palletCode,
          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/OutboundBatchPicking/split-package', {
                orderNo: this.splitForm.orderNo,
                palletCode: this.splitForm.palletCode,
                originalBarcode: this.splitForm.originalBarcode,
                splitQuantity: this.splitForm.splitQuantity
              });
              if (res.status) {
                this.$message.success('拆包成功');
                this.showCustomSplitDialog = false;
                await this.loadPalletData();
              } else {
                this.$message.error(res.message || '拆包失败');
              }
            } catch (error) {
              this.$message.error('拆包失败');
            } finally {
              this.splitLoading = false;
            }
          }
        });
      }
    },
// åœ¨æ‹†åŒ…弹窗中查看拆包链
async viewSplitChainFromSplit(barcode) {
  if (!barcode) {
    this.$message.warning('请先输入条码');
    return;
  }
  // å…ˆå…³é—­æ‹†åŒ…弹窗
  this.closeCustomSplitDialog();
  await this.$nextTick();
  // ç„¶åŽæ‰“开拆包链信息弹窗
  await this.viewSplitChain(barcode);
},
    // æ’¤é”€æ‹†åŒ…
    async onRevertSplitBarcodeScan() {
      if (!this.revertSplitForm.newBarcode) return;
      this.revertSplitForm.newBarcode = this.revertSplitForm.newBarcode.replace(/\n/g, '').trim();
      // æ–°å¢žï¼šæ‰«æåŽè‡ªåŠ¨æ˜¾ç¤ºæ‹†åŒ…é“¾ä¿¡æ¯
      await this.viewSplitChain(this.revertSplitForm.newBarcode);
    },
    async handleRevertSplit() {
      if (this.$refs.revertSplitFormRef) {
        this.$refs.revertSplitFormRef.validate(async (valid) => {
          if (valid) {
            this.revertSplitLoading = true;
            try {
              const res = await http.post('/api/OutboundBatchPicking/cancel-split', {
                orderNo: this.scanData.orderNo,
                palletCode: this.scanData.palletCode,
                newBarcode: this.revertSplitForm.newBarcode
              });
              if (res.status) {
                this.$message.success('撤销拆包成功');
                this.showRevertSplitDialog = false;
                await this.loadPalletData();
              } else {
                this.$message.error(res.message || '撤销拆包失败');
              }
            } catch (error) {
              this.$message.error('撤销拆包失败');
            } finally {
              this.revertSplitLoading = false;
            }
          }
        });
      }
    },
// æŸ¥æ‰¾å®Œæ•´æ‹†åŒ…链(从根条码开始)
async findRootChain(currentBarcode) {
  this.splitChainLoading = true;
  try {
    const res = await http.post('/api/OutboundBatchPicking/find-root-split-chain', {
      orderNo: this.scanData.orderNo,
      barcode: currentBarcode
    });
    if (res.status) {
      this.splitChainInfo = res.data;
      this.$message.success('已加载完整拆包链');
    } else {
      this.$message.error(res.message || '查找完整拆包链失败');
    }
  } catch (error) {
    this.$message.error('查找完整拆包链失败');
  } finally {
    this.splitChainLoading = false;
  }
},
    // æ‹†åŒ…链相关方法
   // æŸ¥çœ‹æ‹†åŒ…链信息
async viewSplitChain(barcode) {
  if (!barcode) {
    this.$message.warning('请先输入条码');
    return;
  }
  this.splitChainLoading = true;
  try {
    const res = await http.post('/api/OutboundBatchPicking/split-package-chain-info', {
      orderNo: this.scanData.orderNo,
      barcode: barcode
    });
    if (res.status) {
      this.splitChainInfo = res.data;
      // æ˜¾ç¤ºæç¤ºä¿¡æ¯ï¼Œå‘Šè¯‰ç”¨æˆ·è¿™æ˜¯ä»€ä¹ˆç±»åž‹çš„æ‹†åŒ…链
      let chainType = "当前条码的拆包链";
      if (this.splitChainInfo.chainType === 'root') {
        chainType = "完整拆包链(从原始条码开始)";
      } else if (this.splitChainInfo.chainType === 'branch') {
        chainType = "分支拆包链";
      }
      this.$message.info(`已加载${chainType},共${this.splitChainInfo.totalSplitTimes}次拆包`);
      this.showSplitChainDialog = true;
    } else {
      this.$message.error(res.message || '获取拆包链信息失败');
    }
  } catch (error) {
    this.$message.error('获取拆包链信息失败');
  } finally {
    this.splitChainLoading = false;
  }
},
    // å…³é—­æ‹†åŒ…链信息弹窗
    closeSplitChainDialog() {
      this.showSplitChainDialog = false;
    },
    // åœ¨æ’¤é”€æ‹†åŒ…弹窗中查看拆包链
async viewSplitChainFromRevert(barcode) {
  if (!barcode) {
    this.$message.warning('请先输入条码');
    return;
  }
  // å…ˆå…³é—­æ’¤é”€æ‹†åŒ…弹窗
  this.closeRevertSplitDialog();
  await this.$nextTick();
  // ç„¶åŽæ‰“开拆包链信息弹窗
  await this.viewSplitChain(barcode);
},
// å¿«é€Ÿé‡æ–°æ‰“开拆包链弹窗
async quickReopenSplitChainDialog(barcode) {
  if (!barcode) return;
  this.showSplitChainDialog = true;
  this.splitChainLoading = true;
  try {
    const res = await http.post('/api/OutboundBatchPicking/split-package-chain-info', {
      orderNo: this.scanData.orderNo,
      barcode: barcode
    });
    if (res.status) {
      this.splitChainInfo = res.data;
    }
  } catch (error) {
    console.error('重新加载拆包链信息失败:', error);
  } finally {
    this.splitChainLoading = false;
  }
},
    // å–消单个拆包记录
async cancelSingleSplit(newBarcode) {
  // å…ˆè®°å½•当前信息,然后关闭弹窗
  const originalBarcode = this.splitChainInfo.originalBarcode;
  this.closeSplitChainDialog();
  await this.$nextTick();
  try {
    await this.$confirm(
      `确定要取消条码 ${newBarcode} çš„æ‹†åŒ…操作吗?`,
      '取消单个拆包',
      {
        confirmButtonText: '确定取消',
        cancelButtonText: '再想想',
        type: 'warning'
      }
    );
    this.revertSplitLoading = true;
    const res = await http.post('/api/OutboundBatchPicking/cancel-split', {
      orderNo: this.scanData.orderNo,
      palletCode: this.scanData.palletCode,
      newBarcode: newBarcode
    });
    if (res.status) {
      this.$message.success('取消拆包成功');
      await this.loadPalletData();
      // é‡æ–°æ‰“开弹窗显示更新后的状态
      await this.viewSplitChain(originalBarcode);
    } else {
      this.$message.error(res.message || '取消拆包失败');
      await this.viewSplitChain(originalBarcode);
    }
  } catch (error) {
    if (error === 'cancel') {
      // ç”¨æˆ·å–消后重新打开弹窗
      await this.viewSplitChain(originalBarcode);
    } else {
      this.$message.error('取消拆包失败');
      await this.viewSplitChain(originalBarcode);
    }
  } finally {
    this.revertSplitLoading = false;
  }
},
// å–消整个拆包链
async cancelWholeSplitChain() {
  // å…ˆè®°å½•当前拆包链信息,然后关闭弹窗
  const originalBarcode = this.splitChainInfo.originalBarcode;
  this.closeSplitChainDialog();
  // ç»™ä¸€ç‚¹æ—¶é—´è®©å¼¹çª—完全关闭
  await this.$nextTick();
  try {
    // çŽ°åœ¨æ˜¾ç¤ºç¡®è®¤å¯¹è¯æ¡†ï¼Œç¡®ä¿å®ƒåœ¨æœ€å‰é¢
    await this.$confirm(
      `确定要取消整个拆包链吗?\n这将取消从条码 ${originalBarcode} å¼€å§‹çš„æ‰€æœ‰æ‹†åŒ…操作。`,
      '取消拆包链确认',
      {
        confirmButtonText: '确定取消',
        cancelButtonText: '再想想',
        type: 'warning',
        center: true,
        closeOnClickModal: false
      }
    );
    // ç”¨æˆ·ç¡®è®¤åŽæ‰§è¡Œå–消操作
    this.revertSplitLoading = true;
    const res = await http.post('/api/OutboundBatchPicking/cancel-split-chain', {
      orderNo: this.scanData.orderNo,
      palletCode: this.scanData.palletCode,
      startBarcode: originalBarcode
    });
    console.log('取消拆包链响应:', res);
    if (res.status) {
      this.$message.success('取消拆包链成功');
      await this.loadPalletData();
      // å¯é€‰ï¼šé‡æ–°æ‰“开拆包链信息弹窗显示更新后的状态
      // await this.viewSplitChain(originalBarcode);
    } else {
      this.$message.error(res.message || '取消拆包链失败');
      // å¤±è´¥åŽé‡æ–°æ‰“开弹窗
      await this.viewSplitChain(originalBarcode);
    }
  } catch (error) {
    // ç”¨æˆ·å–消操作
    if (error === 'cancel') {
      console.log('用户取消了拆包链操作');
      // ç”¨æˆ·å–消后重新打开弹窗
      await this.viewSplitChain(originalBarcode);
    } else {
      console.error('取消拆包链错误:', error);
      this.$message.error('取消拆包链失败: ' + error.message);
      // å‡ºé”™åŽé‡æ–°æ‰“开弹窗
      await this.viewSplitChain(originalBarcode);
    }
  } finally {
    this.revertSplitLoading = false;
  }
},
    // æ£€æŸ¥æ¡ç æ˜¯å¦å·²è¢«åˆ†æ‹£
    hasPicked(barcode) {
      return this.pickedList.some(item => item.currentBarcode === barcode);
    },
    // æ ¼å¼åŒ–日期时间
    formatDateTime(dateTime) {
      if (!dateTime) return '';
      const date = new Date(dateTime);
      return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`;
    },
    // å›žåº“相关方法
    openBatchReturnDialog() {
      if (!this.scanData.palletCode) {
        this.$message.warning('请先扫描托盘码');
        return;
      }
      this.showBatchReturnDialog = true;
      this.batchReturnForm.orderNo = this.scanData.orderNo;
      this.batchReturnForm.palletCode = this.scanData.palletCode;
      this.batchReturnForm.unpickedCount = this.summary.unpickedCount;
      this.batchReturnForm.unpickedQuantity = this.summary.unpickedQuantity;
    },
    async handleBatchReturnConfirm() {
      this.batchReturnLoading = true;
      try {
        const res = await http.post('/api/OutboundBatchPicking/return-stock', {
          orderNo: this.scanData.orderNo,
          palletCode: this.scanData.palletCode
        });
        if (res.status) {
          this.$message.success('回库成功');
          this.showBatchReturnDialog = false;
          await this.loadPalletData();
        } else {
          this.$message.error(res.message || '回库失败');
        }
      } catch (error) {
        this.$message.error('回库失败');
      } finally {
        this.batchReturnLoading = false;
      }
    },
    // å–空箱方法
    handleEmptyPallet() {
      this.showEmptyPalletDialog = true;
      this.emptypalletOutForm.orderNo = this.scanData.orderNo;
      this.emptypalletOutForm.palletCode = '';
    },
    async handleEmptyPalletConfirm() {
      this.emptypalletOutLoading = true;
      try {
        const res = await http.post('/api/OutboundBatchPicking/remove-empty-pallet', {
          orderNo: this.emptypalletOutForm.orderNo,
          palletCode: this.emptypalletOutForm.palletCode
        });
        if (res.status) {
          this.$message.success('取走空箱成功');
          this.showEmptyPalletDialog = false;
          await this.loadPalletData();
        } else {
          this.$message.error(res.message || '取走空箱失败');
        }
      } catch (error) {
        this.$message.error('取走空箱失败');
      } finally {
        this.emptypalletOutLoading = false;
      }
    },
    // æ•°æ®åŠ è½½æ–¹æ³•
    async loadPalletData() {
      if (!this.scanData.orderNo || !this.scanData.palletCode) return;
      await this.loadUnpickedList();
      await this.loadPickedList();
      await this.loadPalletStatus();
    },
    async loadUnpickedList() {
      try {
        const res = await http.post('/api/OutboundBatchPicking/pallet-locks', {
          orderNo: this.scanData.orderNo,
          palletCode: this.scanData.palletCode
        });
        if (res.status) {
          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/OutboundBatchPicking/pallet-picked-list', {
          orderNo: this.scanData.orderNo,
          palletCode: this.scanData.palletCode
        });
        if (res.status) {
          this.pickedList = res.data || [];
          this.summary.pickedCount = this.pickedList.length;
        }
      } catch (error) {
        this.$message.error('加载已拣选列表失败');
      }
    },
    async loadPalletStatus() {
      try {
        const res = await http.post('/api/OutboundBatchPicking/pallet-status', {
          orderNo: this.scanData.orderNo,
          palletCode: this.scanData.palletCode
        });
        if (res.status) {
          this.palletStatus = res.data.statusText || '未知';
        }
      } catch (error) {
        this.palletStatus = '未知';
      }
    },
    // æ‰«ç ç›¸å…³æ–¹æ³•
    onPalletScan() {
      this.scanData.palletCode = this.scanData.palletCode.replace(/\n/g, '').trim();
      if (!this.scanData.palletCode) return;
      this.loadPalletData();
      this.$nextTick(() => {
        this.$refs.barcodeInput.focus();
      });
    },
    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/OutboundBatchPicking/cancel-picking', {
                orderNo: this.scanData.orderNo,
                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.loadPalletData();
          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;
    },
    onEmptyPalletScan() {
      if (!this.emptypalletOutForm.palletCode) return;
      this.emptypalletOutForm.palletCode = this.emptypalletOutForm.palletCode.replace(/\n/g, '').trim();
    },
    closeEmptyPalletDialog() {
      this.showEmptyPalletDialog = false;
      this.emptypalletOutForm.palletCode = '';
    },
    parentcall() {
      // æ‰“印回调
    }
  }
})
</script>
<style scoped>
.OutboundPicking-container {
  padding: 20px;
}
.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;
}
/* è‡ªå®šä¹‰å¼¹çª—样式 */
:deep(.el-message-box) {
  z-index: 10010 !important;
}
:deep(.el-overlay) {
  z-index: 10009 !important;
}
:deep(.el-message) {
  z-index: 10011 !important;
}
.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: 2000; /* ä¿æŒä¸€ä¸ªåˆç†çš„ z-index */
}
.custom-dialog-wrapper {
  position: relative;
  z-index: 2001;
}
.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;
  }
  /* ç¡®ä¿ç¡®è®¤å¯¹è¯æ¡†åœ¨æœ€å‰é¢ */
.el-message-box__wrapper {
  z-index: 10001 !important;
}
.el-message {
  z-index: 10002 !important;
}
  .scanner-form .el-input {
    width: 100%;
  }
}
</style>
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/outbound/PickingConfirm.vue
@@ -28,7 +28,7 @@
          <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>
          <!-- <el-button type="danger" @click="handleDirectOutbound">直接出库</el-button>  -->
@@ -250,6 +250,49 @@
        </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>
  </div>
    <!-- ç›´æŽ¥å‡ºåº“弹窗 -->
    <div v-if="showDirectOutDialog" class="custom-dialog-overlay">
      <div class="custom-dialog-wrapper">
@@ -290,7 +333,7 @@
        </div>
      </div>
    </div>
  </div>
  <print-view ref="childs" @parentcall="parentcall"></print-view>
</template>
@@ -431,6 +474,20 @@
          { required: true, validator: validateDirectOutPalletCode, trigger: 'blur' }
        ]
      },
      showEmptyPalletDialog: false, // å–走空箱弹窗显示状态
      emptypalletOutLoading: false, // å–走空箱加载状态
      emptypalletOutForm: {
        orderNo: '',
        palletCode: ''
      },
      emptypalletOutFormRules: {
        palletCode: [
          { required: true, validator: validateDirectOutPalletCode, trigger: 'blur' }
        ]
      },
      returnForm: {
        orderNo: '',
        palletCode: '',
@@ -598,7 +655,7 @@
        });
        
        if (res.status) {
          this.$message.success('回库成功');
          this.$message.success(res.message);
          this.showBatchReturnDialog = false;
          this.loadData();
        } else {
@@ -708,6 +765,109 @@
    handleDirectOutbound() {
      this.openDirectOutDialog();
    },
   // æ‰“开取走空箱弹窗
    openEmptyPalletDialog() {
      console.log('打开取走空箱弹窗');
      this.showEmptyPalletDialog = true;
      // é‡ç½®è¡¨å•
      this.resetEmptyPalletForm();
      // è®¾ç½®è®¢å•信息
      this.emptypalletOutForm.orderNo = this.scanData.orderNo;
      // æ¸…除表单验证
      this.$nextTick(() => {
        if (this.$refs.emptyPalletFormRef) {
          this.$refs.emptyPalletFormRef.clearValidate();
        }
      });
    },
    // å…³é—­å–走空箱弹窗
    closeEmptyPalletDialog() {
      this.showEmptyPalletDialog = false;
      this.resetEmptyPalletForm();
      // æ¸…除表单验证
      if (this.$refs.emptyPalletFormRef) {
        this.$refs.emptyPalletFormRef.clearValidate();
      }
    },
    // å–走空箱托盘码扫码
    onEmptyPalletScan() {
      if (!this.emptypalletOutForm.palletCode) return;
      this.emptypalletOutForm.palletCode = this.emptypalletOutForm.palletCode.replace(/\n/g, '').trim();
      // æ¸…除验证状态
      if (this.$refs.emptyPalletFormRef) {
        this.$refs.emptyPalletFormRef.clearValidate(['palletCode']);
      }
    },
    // å–走空箱确认
    async handleEmptyPalletConfirm() {
      // è¡¨å•验证
      if (this.$refs.emptyPalletFormRef) {
        this.$refs.emptyPalletFormRef.validate((valid) => {
          if (valid) {
            this.submitEmptyPallet();
          } else {
            this.$message.warning('请扫描托盘码');
            return false;
          }
        });
      } else {
        // å¦‚果没有表单引用,使用原有的验证
        if (!this.emptypalletOutForm.palletCode) {
          this.$message.warning('请扫描托盘码');
          return;
        }
        this.submitEmptyPallet();
      }
    },
    // æäº¤å–走空箱请求
    async submitEmptyPallet() {
      this.emptypalletOutLoading = true;
      try {
        const res = await this.http.post('/api/OutboundPicking/remove-empty-pallet', {
          orderNo: this.emptypalletOutForm.orderNo,
          palletCode: this.emptypalletOutForm.palletCode
        });
        debugger;
        if (res.status) {
          this.$message.success('取走空箱成功');
          this.showEmptyPalletDialog = false;
          this.loadData();
        } else {
          this.$message.error(res.message || '取走空箱失败');
        }
      } catch (error) {
        this.$message.error('取走空箱失败');
      } finally {
        this.emptypalletOutLoading = false;
      }
    },
    // é‡ç½®å–走空箱表单
    resetEmptyPalletForm() {
      this.emptypalletOutForm.palletCode = '';
    },
    // ä¿®æ”¹åŽŸæœ‰çš„å–èµ°ç©ºç®±æŒ‰é’®ç‚¹å‡»äº‹ä»¶
    handleEmptyPallet() {
      this.openEmptyPalletDialog();
    },
    async loadData() {
      if (!this.scanData.orderNo || !this.scanData.palletCode) {
        return;
@@ -775,21 +935,16 @@
              
              if (res.status) {
                successCount++;
                 this.$message.success(`成功取消`);
              } else {
                errorCount++;
                console.error(`取消拣选失败: ${row.Barcode}`, res.message);
                 this.$message.warning(`取消拣选失败: ${row.currentBarcode} - ${res.message}`);
              }
            } catch (error) {
              errorCount++;
              console.error(`取消拣选失败: ${row.Barcode}`, error);
              this.$message.warning(`取消拣选失败: ${row.currentBarcode} - ${error.message}` );
            }
          }
          if (errorCount === 0) {
            this.$message.success(`成功取消 ${successCount} é¡¹`);
          } else {
            this.$message.warning(`成功取消 ${successCount} é¡¹ï¼Œå¤±è´¥ ${errorCount} é¡¹`);
          }
          }
          
          this.loadData();
          this.selectedPickedRows = [];
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/outbound/allocateoutboundOrder.vue
@@ -38,7 +38,7 @@
    const table = ref({
      key: "id",
      footer: "Foots",
      cnName: "调拨单(外部仓库调智仓)",
      cnName: "调拨单(智仓调出外部仓)",
      name: "outboundOrder",
      url: "/OutboundOrder/",
      sortName: "id",
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/outbound/outboundOrder.vue
@@ -114,6 +114,7 @@
          type: "select",
          dataKey: "outOrderType",
          data: [],
          hidden:true
        },
        {
          title: "单据状态",
@@ -168,6 +169,7 @@
        width: 150,
        align: "left",
        bind: { key: "outOrderType", data: [] },
        hidden:true
      },
      {
        field: "businessType",
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/record/locationStatusChangeRecord.vue
@@ -44,9 +44,9 @@
    });
    const searchFormOptions = ref([
      [
        { title: "货位号", field: "locationCode" },
        { title: "单据编号", field: "orderNo" },
        { title: "任务号", field: "taskNum" },
        { title: "货位号", field: "locationCode" ,type:"like"},
        { title: "单据编号", field: "orderNo",type:"like"},
        { title: "任务号", field: "taskNum" ,type:"like"},
      ],
      [
        {
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/record/stockQuantityChangeRecord.vue
@@ -39,15 +39,15 @@
      });
      const searchFormOptions = ref([
        [
          { title: "托盘号", field: "palleCode" },
          { title: "物料编号", field: "materielCode" },
          { title: "单据编号", field: "orderNo" },
          { title: "托盘号", field: "palleCode" ,type:"like"},
          { title: "物料编号", field: "materielCode",type:"like" },
          { title: "单据编号", field: "orderNo" ,type:"like"},
          { title: "变动类型", field: "changeType" ,type: "selectList",dataKey: "stockChangeType",data: [],},
        ],
        [
          { title: "批次号", field: "batchNo" },
          { title: "任务号", field: "taskNum" },
          { title: "序列号", field: "serilNumber" },
          { title: "批次号", field: "batchNo" ,type:"like"},
          { title: "任务号", field: "taskNum" ,type:"like"},
          { title: "序列号", field: "serilNumber" ,type:"like"},
        ],
      ]);
      const columns = ref([
@@ -90,6 +90,13 @@
          align: "left",
        },
        {
          field: "barcode",
          title: "条码",
          type: "string",
          width: 150,
          align: "left",
        },
        {
          field: "batchNo",
          title: "批次号",
          type: "string",
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/stock/stockInfo.vue
@@ -92,6 +92,7 @@
          width: 100,
          align: "left",
          bind: { key: "warehouses", data: [] },
          hidden:true
        },
        {
          field: "creater",
@@ -113,7 +114,7 @@
          type: "string",
          width: 100,
          align: "left",
          hidden:true
          // hidden:true
        },
        {
          field: "modifyDate",
@@ -121,7 +122,7 @@
          type: "datetime",
          width: 160,
          align: "left",
          hidden:true
          // hidden:true
        },
        {
          field: "remark",
ÏîÄ¿´úÂë/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_AllocateService/AllocateService.cs
@@ -1,4 +1,6 @@
using Microsoft.Extensions.Logging;
using Autofac.Core;
using MailKit.Search;
using Microsoft.Extensions.Logging;
using SqlSugar;
using SqlSugar.Extensions;
using System;
@@ -60,7 +62,7 @@
                {
                    1 => await AddAllocateOrder(allocateOrder),
                    2 => await UpdateAllocateOrder(allocateOrder),
                    3 => DeleteAllocateOrder(allocateOrder),
                    3 => await DeleteAllocateOrder(allocateOrder),
                    _ => WebResponseContent.Instance.OK(),
                };
@@ -81,24 +83,7 @@
                }
                allocateOrder.OrderNo = CreateCodeByRule(nameof(RuleCodeEnum.AllocateOrderCodeRule));
                Db.InsertNav(allocateOrder).Include(x => x.Details).ExecuteCommand();
                if (Enum.TryParse<BusinessTypeEnum>(allocateOrder.BusinessType, out var businessType))
                {
                    if (businessType == BusinessTypeEnum.智仓调外部仓库)
                    {
                        var inboundOrders = ConvertToInboundOrders(allocateOrder);
                        await _inboundService.InbounOrderService.ReceiveInboundOrder(inboundOrders, 1);
                    }
                    else if (businessType == BusinessTypeEnum.外部仓库调智仓)
                    {
                        var outboundOrders = ConvertToOutboundOrders(allocateOrder);
                        await _outboundService.OutboundOrderService.ReceiveOutboundOrder(outboundOrders, 1);
                    }
                    else
                    {
                        // å¤„理未定义的枚举值(如未来新增但未实现的类型)
                        throw new NotSupportedException($"不支持的业务类型枚举值: {businessType}");
                    }
                }
                await AddInOutData(allocateOrder);
                return WebResponseContent.Instance.OK();
            }
            catch (Exception ex)
@@ -107,6 +92,39 @@
                return WebResponseContent.Instance.Error(ex.Message);
            }
        }
        private async Task AddInOutData(Dt_AllocateOrder allocateOrder)
        {
            if (Enum.TryParse<BusinessTypeEnum>(allocateOrder.BusinessType, out var businessType))
            {
                if (businessType == BusinessTypeEnum.外部仓库调智仓)
                {
                    allocateOrder.OrderType = InOrderTypeEnum.AllocatInbound.ObjToInt();
                    var inboundOrders = ConvertToInboundOrders(allocateOrder);
                    await _inboundService.InbounOrderService.ReceiveInboundOrder(inboundOrders, 1);
                }
                else if (businessType == BusinessTypeEnum.智仓调外部仓库 || businessType == BusinessTypeEnum.智仓调智仓)
                {
                    if (businessType == BusinessTypeEnum.智仓调外部仓库)
                    {
                        allocateOrder.OrderType = InOrderTypeEnum.AllocatOutbound.ObjToInt();
                    }
                    else if (businessType == BusinessTypeEnum.智仓调智仓)
                    {
                        allocateOrder.OrderType = InOrderTypeEnum.InternalAllocat.ObjToInt();
                    }
                    var outboundOrders = ConvertToOutboundOrders(allocateOrder);
                    await _outboundService.OutboundOrderService.ReceiveOutboundOrder(outboundOrders, 1);
                }
                else
                {
                    // å¤„理未定义的枚举值(如未来新增但未实现的类型)
                    throw new NotSupportedException($"不支持的业务类型枚举值: {businessType}");
                }
            }
        }
        public async Task<WebResponseContent> UpdateAllocateOrder(Dt_AllocateOrder model)
        {
            try
@@ -178,7 +196,7 @@
                _unitOfWorkManage.BeginTran();
                foreach (var item in deletePurchaseOrderDetails)
                {
                   // _allocateOrderDetailRepository.DeleteAndMoveIntoHty(item, OperateTypeEnum.自动删除);
                    // _allocateOrderDetailRepository.DeleteAndMoveIntoHty(item, OperateTypeEnum.自动删除);
                    _allocateOrderDetailRepository.DeleteData(item);
                }
@@ -186,6 +204,10 @@
                _allocateOrderDetailRepository.AddData(allocateOrderDetails);
                BaseDal.UpdateData(allocateOrder);
                DeleteInOutData(model.UpperOrderNo, allocateOrder);
                await AddInOutData(allocateOrder);
                _unitOfWorkManage.CommitTran();
@@ -199,7 +221,7 @@
            }
        }
        public WebResponseContent DeleteAllocateOrder(Dt_AllocateOrder model)
        public async Task<WebResponseContent> DeleteAllocateOrder(Dt_AllocateOrder model)
        {
            try
            {
@@ -221,6 +243,9 @@
                    _allocateOrderDetailRepository.DeleteData(item);
                }
                BaseDal.DeleteData(allocateOrder);
                DeleteInOutData(model.UpperOrderNo, allocateOrder);
                _unitOfWorkManage.CommitTran();
                return WebResponseContent.Instance.OK();
            }
@@ -232,8 +257,33 @@
            }
        }
        private void DeleteInOutData(string upperOrderNo, Dt_AllocateOrder allocateOrder)
        {
            if (Enum.TryParse<BusinessTypeEnum>(allocateOrder.BusinessType, out var businessType))
            {
                if (businessType == BusinessTypeEnum.外部仓库调智仓)
                {
                    _inboundService.InbounOrderService.Db.Deleteable<Dt_InboundOrder>().Where(x => x.UpperOrderNo == upperOrderNo).ExecuteCommand();
                    _inboundService.InboundOrderDetailService.Db.Deleteable<Dt_InboundOrderDetail>()
                     .Where(p => SqlFunc.Subqueryable<Dt_InboundOrder>().Where(s => s.Id == p.OrderId && s.UpperOrderNo == upperOrderNo).Any()).ExecuteCommand();
                }
                else if (businessType == BusinessTypeEnum.智仓调外部仓库 || businessType == BusinessTypeEnum.智仓调智仓)
                {
                    _outboundService.OutboundOrderService.Db.Deleteable<Dt_OutboundOrder>().Where(x => x.UpperOrderNo == upperOrderNo).ExecuteCommand();
                    _outboundService.OutboundOrderDetailService.Db.Deleteable<Dt_OutboundOrderDetail>()
                     .Where(p => SqlFunc.Subqueryable<Dt_OutboundOrder>().Where(s => s.Id == p.OrderId && s.UpperOrderNo == upperOrderNo).Any()).ExecuteCommand();
                }
            }
        }
        public List<Dt_InboundOrder> ConvertToInboundOrders(Dt_AllocateOrder allocateOrder)
        {
            var distinctDetails = allocateOrder.Details
                .GroupBy(d => d.Barcode)
                .Select(g => g.First())
                .ToList();
            return new List<Dt_InboundOrder>()
            {
                new Dt_InboundOrder(){
@@ -241,16 +291,16 @@
                   InboundOrderNo=allocateOrder.OrderNo,
                   UpperOrderNo=allocateOrder.UpperOrderNo,
                   SupplierId=allocateOrder.SupplierId,
                   OrderType=InOrderTypeEnum.Allocat.ObjToInt(),
                   OrderType= allocateOrder.OrderType ,
                   OrderStatus=allocateOrder.OrderStatus,
                   CreateType=allocateOrder.CreateType,
                   BusinessType=allocateOrder.BusinessType,
                   IsBatch=allocateOrder.IsBatch,
                   FactoryArea=allocateOrder.FactoryArea,
                   Remark=allocateOrder.Remark,
                   Details=allocateOrder.Details.Select(detail=>new Dt_InboundOrderDetail
                   Details=distinctDetails.Select(detail=>new Dt_InboundOrderDetail
                   {
                       OrderId= detail.OrderId,
                       OrderId= 0,
                       MaterielCode=detail.MaterielCode,
                       MaterielName="",
                       BatchNo=detail.BatchNo,
@@ -260,6 +310,7 @@
                       OrderDetailStatus=detail.OrderDetailStatus,
                       Unit=detail.Unit,
                       RowNo=0,
                       lineNo=detail.LineNo,
                       SupplyCode=detail.SupplyCode,
                       WarehouseCode=detail.WarehouseCode,
                       Barcode=detail.Barcode,
@@ -273,12 +324,24 @@
        public Dt_OutboundOrder ConvertToOutboundOrders(Dt_AllocateOrder allocateOrder)
        {
            var distinctDetails = allocateOrder.Details
    .GroupBy(d => string.IsNullOrEmpty(d.Barcode)
        ? $"{d.MaterielCode}_{d.BatchNo}_{d.SupplyCode}_{d.WarehouseCode}"
        : d.Barcode)
    .Select(g => new
    {
        Detail = g.First(),
        //汇总分组内的数量
        Qty = g.Sum(x => x.BarcodeQty ?? x.OrderQuantity)
    }).ToList();
            return new Dt_OutboundOrder()
            {
                WarehouseId = allocateOrder.WarehouseId,
                OrderNo = allocateOrder.OrderNo,
                UpperOrderNo = allocateOrder.UpperOrderNo,
                OrderType = OutOrderTypeEnum.Allocate.ObjToInt(),
                OrderType = allocateOrder.OrderType,
                OrderStatus = allocateOrder.OrderStatus,
                CreateType = allocateOrder.CreateType,
                BusinessType = allocateOrder.BusinessType,
@@ -287,20 +350,23 @@
                Remark = allocateOrder.Remark,
                DepartmentCode = "",
                DepartmentName = "",
                Details = allocateOrder.Details.Select(detail => new Dt_OutboundOrderDetail
                Details = distinctDetails.Select(item => new Dt_OutboundOrderDetail
                {
                    OrderId = detail.OrderId,
                    MaterielCode = detail.MaterielCode,
                    OrderId = 0,
                    MaterielCode = item.Detail.MaterielCode,
                    MaterielName = "",
                    BatchNo = detail.BatchNo,
                    OrderQuantity = detail.OrderQuantity,
                    BatchNo = item.Detail.BatchNo,
                    OrderQuantity = item.Detail.OrderQuantity,
                    BarcodeQty = (decimal)item.Detail.BarcodeQty,
                    BarcodeUnit = item.Detail.BarcodeUnit,
                    LockQuantity = 0,
                    lineNo = item.Detail.LineNo,
                    OverOutQuantity = 0,
                    OrderDetailStatus = detail.OrderDetailStatus,
                    Unit = detail.Unit,
                    OrderDetailStatus = item.Detail.OrderDetailStatus,
                    Unit = item.Detail.Unit,
                    RowNo = 0,
                    SupplyCode = detail.SupplyCode,
                    WarehouseCode = detail.WarehouseCode,
                    SupplyCode = item.Detail.SupplyCode,
                    WarehouseCode = item.Detail.WarehouseCode,
                }).ToList()
            };
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_BasicService/DailySequenceService.cs
@@ -10,6 +10,7 @@
using System.Threading.Tasks;
using WIDESEA_Core.BaseRepository;
using WIDESEA_Core.BaseServices;
using WIDESEA_Core.Seed;
using WIDESEA_IBasicService;
using WIDESEA_Model.Models;
using WIDESEA_Model.Models.Basic;
@@ -193,28 +194,43 @@
        private async Task UpdateSequenceInDatabase(DateTime date, string sequenceKey, int value)
        {
            var sequence = await Repository.Db.Queryable<DT_DailySequence>()
                .Where(x => x.SequenceDate == date && x.SequenceKey == sequenceKey)
                .FirstAsync();
            if (sequence != null)
            try
            {
                sequence.CurrentValue = value;
                await Repository.Db.Updateable(sequence).ExecuteCommandAsync();
            }
            else
            {
                // å¦‚果数据库记录不存在,则创建
                sequence = new DT_DailySequence
                using SqlSugarClient sugarClient = new SqlSugarClient(new ConnectionConfig
                {
                    SequenceDate = date,
                    SequenceKey = sequenceKey,
                    CurrentValue = value,
                    IsAutoCloseConnection = true,
                    DbType = DbType.SqlServer,
                    ConnectionString = DBContext.ConnectionString
                });
                var result = await sugarClient.Updateable<DT_DailySequence>()
                    .SetColumns(it => new DT_DailySequence()
                    {
                        CurrentValue = value,
                    })
                    .Where(it => it.SequenceDate == date && it.SequenceKey == sequenceKey)
                    .ExecuteCommandAsync();
                };
                await Repository.Db.Insertable(sequence).ExecuteCommandAsync();
                if (result == 0)
                {
                    // å¦‚果没有更新到记录,可能是首次使用,插入新记录
                    var newSequence = new DT_DailySequence
                    {
                        SequenceKey = sequenceKey,
                        SequenceDate = date,
                        CurrentValue = value,
                        Creater="admin",
                        CreateDate=DateTime.Now,
                    };
                    await sugarClient.Insertable(newSequence).ExecuteCommandAsync();
                }
            }
            catch (Exception ex)
            {
                // è®°å½•日志并重新抛出异常
                Console.WriteLine($"更新序列失败: {ex.Message}");
                throw;
            }
        }
        public async Task CleanOldSequencesAsync(int keepDays = 30)
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_BasicService/InvokeMESService.cs
@@ -10,6 +10,8 @@
using System.Security.Policy;
using System.Text;
using System.Threading.Tasks;
using WIDESEA_Common.OrderEnum;
using WIDESEA_Common.StockEnum;
using WIDESEA_Core;
using WIDESEA_Core.BaseRepository;
using WIDESEA_DTO.Allocate;
@@ -17,6 +19,7 @@
using WIDESEA_DTO.Inbound;
using WIDESEA_DTO.Outbound;
using WIDESEA_IBasicService;
using WIDESEA_IOutboundService;
using WIDESEA_Model.Models;
namespace WIDESEA_BasicService
@@ -32,7 +35,11 @@
        private readonly IRepository<Dt_StockInfoDetail> _stockInfoDetailRepository;
        private readonly IRepository<Dt_StockInfo> _stockInfoRepository;
        private readonly IRepository<Dt_InboundOrder> _inboundOrderRepository;
        public InvokeMESService(IHttpClientFactory httpClientFactory, ILogger<InvokeMESService> logger, IRepository<Dt_FeedbackToMes> feedbacktomesRepository, IRepository<Dt_StockInfoDetail> stockInfoDetailRepository, IRepository<Dt_StockInfo> stockInfoRepository, IRepository<Dt_InboundOrder> inboundOrderRepository)
        private readonly IOutboundOrderService _outboundOrderService;
        private readonly IOutboundOrderDetailService _outboundOrderDetailService;
        private readonly IOutStockLockInfoService _outStockLockInfoService;
        public InvokeMESService(IHttpClientFactory httpClientFactory, ILogger<InvokeMESService> logger, IRepository<Dt_FeedbackToMes> feedbacktomesRepository, IRepository<Dt_StockInfoDetail> stockInfoDetailRepository, IRepository<Dt_StockInfo> stockInfoRepository, IRepository<Dt_InboundOrder> inboundOrderRepository, IOutboundOrderService outboundOrderService, IOutboundOrderDetailService outboundOrderDetailService, IOutStockLockInfoService outStockLockInfoService)
        {
            _httpClientFactory = httpClientFactory;
            _logger = logger;
@@ -40,6 +47,9 @@
            _stockInfoDetailRepository = stockInfoDetailRepository;
            _stockInfoRepository = stockInfoRepository;
            _inboundOrderRepository = inboundOrderRepository;
            _outboundOrderService = outboundOrderService;
            _outboundOrderDetailService = outboundOrderDetailService;
            _outStockLockInfoService = outStockLockInfoService;
        }
        /// <summary>
@@ -50,8 +60,8 @@
        /// <exception cref="HttpRequestException"></exception>
        public async Task<ResponseModel> FeedbackInbound(FeedbackInboundRequestModel model)
        {
            string json =JsonConvert.SerializeObject(model, new JsonSerializerSettings
            {
            string json = JsonConvert.SerializeObject(model, new JsonSerializerSettings
            {
                ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
            });
            var content = new StringContent(json, Encoding.UTF8, "application/json");
@@ -70,7 +80,7 @@
            return JsonConvert.DeserializeObject<ResponseModel>(body);
        }
        /// <summary>
        /// å‡ºåº“反馈
        /// </summary>
@@ -224,7 +234,8 @@
                                        .ToList();
                        var feeds = _feedbacktomesRepository.Db.Queryable<Dt_FeedbackToMes>().Where(x => x.OrderNo == orderNo && x.ReportStatus == 1).Select(o => o.PalletCode).ToList();
                        var unreports = stockinfos.Where(x => !feeds.Contains(x.PalletCode)).ToList();
                        if (unreports!=null && !unreports.Any()) {
                        if (unreports != null && !unreports.Any())
                        {
                            return WebResponseContent.Instance.Error("没有需要回传的数据");
                        }
                        foreach (var item in unreports)
@@ -241,14 +252,14 @@
                                        reqTime = DateTime.Now.ToString(),
                                        business_type = inboundOrder.BusinessType,
                                        factoryArea = inboundOrder.FactoryArea,
                                        operationType=1,
                                        Operator= inboundOrder.Operator,
                                        operationType = 1,
                                        Operator = inboundOrder.Operator,
                                        orderNo = inboundOrder.UpperOrderNo,
                                        status = inboundOrder.OrderStatus,
                                        details = new List<FeedbackInboundDetailsModel>()
                                    };
                                    var groupedData = lists.GroupBy(item => new { item.MaterielCode, item.SupplyCode, item.BatchNo, item.InboundOrderRowNo, item.BarcodeUnit, item.WarehouseCode })
                                       .Select(group => new FeedbackInboundDetailsModel
                                       {
@@ -256,9 +267,9 @@
                                           supplyCode = group.Key.SupplyCode,
                                           batchNo = group.Key.BatchNo,
                                           lineNo = group.Key.InboundOrderRowNo,
                                           qty = group.Sum(x=>x.BarcodeQty),
                                           qty = group.Sum(x => x.BarcodeQty),
                                           // warehouseCode = group.Key.WarehouseCode=="0"?"1072": group.Key.WarehouseCode,
                                           warehouseCode =group.Key.WarehouseCode,
                                           warehouseCode = group.Key.WarehouseCode,
                                           unit = group.Key.BarcodeUnit,
                                           barcodes = group.Select(row => new FeedbackBarcodesModel
                                           {
@@ -284,7 +295,280 @@
                }
            }
            else if (inout == 2)
            {
                foreach (var orderNo in orderNos)
                {
                    var outboundOrder = await _outboundOrderService.Db.Queryable<Dt_OutboundOrder>().FirstAsync(x => x.OrderNo == orderNo);
                    if (outboundOrder != null && outboundOrder.IsBatch == 0)
                    {
                        await HandleOutboundOrderToMESCompletion(outboundOrder, orderNo);
                    }
                    else if (outboundOrder != null && outboundOrder.IsBatch == 1)
                    {
                        await HandleOutboundOrderBatchToMESCompletion(outboundOrder, orderNo);
                    }
                }
            }
            return WebResponseContent.Instance.OK();
        }
        private async Task HandleOutboundOrderBatchToMESCompletion(Dt_OutboundOrder outboundOrder, string orderNo)
        {
            try
            {
                if (outboundOrder.ReturnToMESStatus == 1)
                {
                    return;
                }
                var orderDetails = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>()
                    .LeftJoin<Dt_OutboundOrder>((o, item) => o.OrderId == item.Id)
                    .Where((o, item) => item.OrderNo == orderNo && item.ReturnToMESStatus != 1)
                    .Select((o, item) => o)
                    .ToListAsync();
                var detailids = new List<int>();
                var allCompleted = true;
                foreach (var detail in orderDetails.Where(x => x.ReturnToMESStatus == 0).ToList())
                {
                    if (detail.OverOutQuantity >= detail.NeedOutQuantity)
                    {
                        detailids.Add(detail.Id);
                    }
                    else
                    {
                        allCompleted = false;
                    }
                }
                if (orderDetails.Any(x => x.ReturnToMESStatus == 2))
                {
                    allCompleted = false;
                }
                int newStatus = allCompleted ? (int)OutOrderStatusEnum.出库完成 : (int)OutOrderStatusEnum.出库中;
                if (outboundOrder.OrderStatus != newStatus)
                {
                    await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
                        .SetColumns(x => x.OrderStatus == newStatus)
                        .Where(x => x.OrderNo == orderNo)
                        .ExecuteCommandAsync();
                }
                var documentno = UniqueValueGenerator.Generate();
                var feedmodel = new FeedbackOutboundRequestModel
                {
                    reqCode = Guid.NewGuid().ToString(),
                    reqTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
                    business_type = outboundOrder.BusinessType,
                    factoryArea = outboundOrder.FactoryArea,
                    operationType = 1,
                    Operator = outboundOrder.Operator,
                    orderNo = outboundOrder.UpperOrderNo,
                    documentsNO = documentno,
                    status = outboundOrder.OrderStatus,
                    details = new List<FeedbackOutboundDetailsModel>()
                };
                foreach (var detail in orderDetails.Where(x => detailids.Contains(x.Id)).ToList())
                {
                    // èŽ·å–è¯¥æ˜Žç»†å¯¹åº”çš„æ¡ç ä¿¡æ¯ï¼ˆä»Žé”å®šè®°å½•ï¼‰
                    var detailLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                        .Where(x => x.OrderNo == orderNo && detailids.Contains(x.OrderDetailId) &&
                                   x.Status == (int)OutLockStockStatusEnum.拣选完成)
                        .ToListAsync();
                    var detailModel = new FeedbackOutboundDetailsModel
                    {
                        materialCode = detail.MaterielCode,
                        lineNo = detail.lineNo, // æ³¨æ„ï¼šè¿™é‡Œå¯èƒ½éœ€è¦è°ƒæ•´å­—段名
                        warehouseCode = detail.WarehouseCode,
                        qty = detail.OverOutQuantity, // ä½¿ç”¨è®¢å•明细的已出库数量
                        currentDeliveryQty = detail.OverOutQuantity,
                        unit = detail.Unit,
                        barcodes = detailLocks.Select(lockInfo => new WIDESEA_DTO.Outbound.BarcodesModel
                        {
                            barcode = lockInfo.CurrentBarcode,
                            supplyCode = lockInfo.SupplyCode,
                            batchNo = lockInfo.BatchNo,
                            unit = lockInfo.Unit,
                            qty = lockInfo.PickedQty // æ¡ç çº§åˆ«çš„æ•°é‡ä»ç”¨é”å®šè®°å½•
                        }).ToList()
                    };
                    feedmodel.details.Add(detailModel);
                }
                var result = await FeedbackOutbound(feedmodel);
                if (result != null && result.code == 200)
                {
                    await _outboundOrderDetailService.Db.Updateable<Dt_OutboundOrderDetail>()
                           .SetColumns(it => new Dt_OutboundOrderDetail
                           {
                               ReturnToMESStatus = 1,
                               documentsNO = documentno,
                           })
                        .Where(x => detailids.Contains(x.Id))
                        .ExecuteCommandAsync();
                    if (allCompleted && newStatus == (int)OutOrderStatusEnum.出库完成)
                    {
                        await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
                          .SetColumns(x => x.ReturnToMESStatus == 1)
                          .Where(x => x.OrderNo == orderNo)
                          .ExecuteCommandAsync();
                    }
                }
                else
                {
                    await _outboundOrderDetailService.Db.Updateable<Dt_OutboundOrderDetail>()
                         .SetColumns(it => new Dt_OutboundOrderDetail
                         {
                             ReturnToMESStatus = 2,
                             documentsNO = documentno,
                         })
                      .Where(x => detailids.Contains(x.Id))
                      .ExecuteCommandAsync();
                }
            }
            catch (Exception ex)
            {
                _logger.LogError($"CheckAndUpdateOrderStatus失败 - OrderNo: {orderNo}, Error: {ex.Message}");
            }
        }
        private async Task HandleOutboundOrderToMESCompletion(Dt_OutboundOrder outboundOrder, string orderNo)
        {
            try
            {
                if (outboundOrder.ReturnToMESStatus == 1)
                {
                    return;
                }
                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)
                    .ToListAsync();
                bool allCompleted = true;
                foreach (var detail in orderDetails)
                {
                    if (detail.OverOutQuantity < detail.NeedOutQuantity)
                    {
                        allCompleted = false;
                        break;
                    }
                }
                int newStatus = allCompleted ? (int)OutOrderStatusEnum.出库完成 : (int)OutOrderStatusEnum.出库中;
                if (outboundOrder.OrderStatus != newStatus)
                {
                    await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
                        .SetColumns(x => x.OrderStatus == newStatus)
                        .Where(x => x.OrderNo == orderNo)
                        .ExecuteCommandAsync();
                }
                //只有正常分拣完成时才向MES反馈
                if (allCompleted && newStatus == (int)OutOrderStatusEnum.出库完成)
                {
                    var feedmodel = new FeedbackOutboundRequestModel
                    {
                        reqCode = Guid.NewGuid().ToString(),
                        reqTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
                        business_type = outboundOrder.BusinessType,
                        factoryArea = outboundOrder.FactoryArea,
                        operationType = 1,
                        Operator = outboundOrder.Operator,
                        orderNo = outboundOrder.UpperOrderNo,
                        documentsNO = outboundOrder.OrderNo,
                        status = outboundOrder.OrderStatus,
                        details = new List<FeedbackOutboundDetailsModel>()
                    };
                    foreach (var detail in orderDetails)
                    {
                        // èŽ·å–è¯¥æ˜Žç»†å¯¹åº”çš„æ¡ç ä¿¡æ¯ï¼ˆä»Žé”å®šè®°å½•ï¼‰
                        var detailLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                            .Where(x => x.OrderNo == orderNo &&
                                       x.OrderDetailId == detail.Id &&
                                       x.Status == (int)OutLockStockStatusEnum.拣选完成)
                            .ToListAsync();
                        var detailModel = new FeedbackOutboundDetailsModel
                        {
                            materialCode = detail.MaterielCode,
                            lineNo = detail.lineNo, // æ³¨æ„ï¼šè¿™é‡Œå¯èƒ½éœ€è¦è°ƒæ•´å­—段名
                            warehouseCode = detail.WarehouseCode,
                            qty = detail.OverOutQuantity, // ä½¿ç”¨è®¢å•明细的已出库数量
                            currentDeliveryQty = detail.OverOutQuantity,
                            unit = detail.Unit,
                            barcodes = detailLocks.Select(lockInfo => new WIDESEA_DTO.Outbound.BarcodesModel
                            {
                                barcode = lockInfo.CurrentBarcode,
                                supplyCode = lockInfo.SupplyCode,
                                batchNo = lockInfo.BatchNo,
                                unit = lockInfo.Unit,
                                qty = lockInfo.PickedQty // æ¡ç çº§åˆ«çš„æ•°é‡ä»ç”¨é”å®šè®°å½•
                            }).ToList()
                        };
                        feedmodel.details.Add(detailModel);
                    }
                    var result = await FeedbackOutbound(feedmodel);
                    if (result != null && result.code == 200)
                    {
                        await _outboundOrderDetailService.Db.Updateable<Dt_OutboundOrderDetail>()
                            .SetColumns(x => x.ReturnToMESStatus == 1)
                            .Where(x => x.OrderId == outboundOrder.Id)
                            .ExecuteCommandAsync();
                        await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
                            .SetColumns(x => x.ReturnToMESStatus == 1)
                            .Where(x => x.OrderNo == orderNo)
                            .ExecuteCommandAsync();
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError($"CheckAndUpdateOrderStatus失败 - OrderNo: {orderNo}, Error: {ex.Message}");
            }
        }
    }
    public static class UniqueValueGenerator
    {
        // åŽŸå­è®¡æ•°å™¨ï¼ˆçº¿ç¨‹å®‰å…¨ï¼Œæ¯æ¬¡é€’å¢ž1,避免同一Ticks重复)
        private static long _counter = 0;
        /// <summary>
        /// ç”Ÿæˆå”¯ä¸€å€¼ï¼ˆæ”¯æŒé«˜å¹¶å‘)
        /// </summary>
        /// <returns>格式:yyyyMMdd + Ticks + 3位计数器(如2025112563867890123001)</returns>
        public static string Generate()
        {
            var now = DateTime.Now;
            string datePart = now.ToString("yyyyMMdd");
            long ticksPart = now.Ticks;
            // åŽŸå­é€’å¢žè®¡æ•°å™¨ï¼ˆå–æ¨¡1000,确保计数器仅3位,控制长度)
            long counterPart = Interlocked.Increment(ref _counter) % 1000;
            // æ‹¼æŽ¥ï¼šè®¡æ•°å™¨è¡¥0为3位(避免位数不一致)
            return $"{datePart}{ticksPart}{counterPart:D3}";
        }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_BasicService/LocationInfoService.cs
@@ -189,6 +189,10 @@
                if (first != null)
                {
                    locationCaches.Add(new LocationCache { LocationCode = first.LocationCode, DateTime = DateTime.Now });
                    Db.Updateable<Dt_LocationInfo>().SetColumns(x => new Dt_LocationInfo
                    {
                        LocationStatus = (int)LocationStatusEnum.InStockLock,
                    }).Where(x => x.Id == first.Id).ExecuteCommand();
                }
                return first;
@@ -213,7 +217,7 @@
        {
            return Repository.QueryData(x => locationCodes.Contains(x.LocationCode));
        }
        public List<LocationTypeDto> GetLocationTypes()
        {
            return _locationTypeRepository.Db.Queryable<Dt_LocationType>().Select(x =>
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_BasicService/MaterialUnitService.cs
@@ -156,6 +156,18 @@
            return ConvertAsync(materialData, quantity, materialData.PurchaseUnit, materialData.StockUnit);
        }
        public async Task<MaterialWithUnitConversionResult> ConvertFromToStockAsync(string materialCode,string  fromUom, decimal quantity)
        {
            var materialData = await GetMaterialWithUnitsAsync(materialCode);
            // å¦‚果领料单位和库存单位相同,直接返回
            if (fromUom.Equals(materialData.StockUnit, StringComparison.OrdinalIgnoreCase))
                return new MaterialWithUnitConversionResult(quantity, materialData.StockUnit, false);
            return ConvertAsync(materialData, quantity, fromUom, materialData.StockUnit);
        }
        /// <summary>
        /// é¢†æ–™å•位转库存单位
        /// </summary>
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_BasicService/WIDESEA_BasicService.csproj
@@ -8,6 +8,7 @@
  <ItemGroup>
    <ProjectReference Include="..\WIDESEA_IBasicService\WIDESEA_IBasicService.csproj" />
    <ProjectReference Include="..\WIDESEA_IOutboundService\WIDESEA_IOutboundService.csproj" />
    <ProjectReference Include="..\WIDESEA_IRecordService\WIDESEA_IRecordService.csproj" />
  </ItemGroup>
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Common/CommonEnum/PalletTypeEnum.cs
@@ -35,4 +35,18 @@
        /// </summary>
        LargestPallet = 4
    }
    public enum PalletStatusEnum
    {
        æœªå¼€å§‹ = 0,
        æ‹£é€‰ä¸­ = 1,
        å·²å®Œæˆ = 2,
        æ— ä»»åŠ¡ = 3
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Common/OrderEnum/InboundOrderMenu.cs
@@ -70,11 +70,17 @@
        /// è°ƒæ‹¨å…¥åº“单
        /// </summary>
        [Description("调拨入库单")]
        Allocat = 115,
        AllocatInbound = 115,
        [Description("调拨出库单")]
        AllocatOutbound = 215,
        [Description("重检回库")]
        ReCheck =116,
        [Description("智仓调智仓入库单")]
        InternalAllocat = 117,
        /// <summary>
        /// é”€å”®é€€è´§å•
        /// </summary>
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Common/OrderEnum/OutboundOrderEnum.cs
@@ -21,11 +21,17 @@
        [Description("出库中")]
        å‡ºåº“中 = 1,
        /// <summary>
        /// å‡ºåº“完成
        /// </summary>
        [Description("出库完成")]
        å‡ºåº“完成 = 2,
        [Description("部分完成")]
        éƒ¨åˆ†å®Œæˆ =3,
        /// <summary>
        /// å…³é—­
@@ -97,4 +103,6 @@
        [Description("其他出库单")]
        Other = 235
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Common/StockEnum/OutLockStockStatusEnum.cs
@@ -16,6 +16,18 @@
    //    å·²å‡ºåº“ = 4,
    //    å·²å›žåº“ = 5
    //}
    // æžšä¸¾å®šä¹‰
    public enum BatchStatusEnum
    {
        åˆ†é…ä¸­ = 0,
        æ‰§è¡Œä¸­ = 1,
        å·²å®Œæˆ = 2,
        å·²å›žåº“ = 3,
        å·²å–消 = 4,
    }
    public enum SplitPackageStatusEnum
    {
        å·²æ‹†åŒ… = 1,
@@ -23,6 +35,8 @@
        å·²æ‹£é€‰ = 3,
        å·²å›žåº“ = 4,
    }
    public enum OutLockStockStatusEnum
    {
        [Description("已分配")]
@@ -52,6 +66,12 @@
        [Description("已回库")]
        å·²å›žåº“ =8,
        [Description("已释放")]
        å·²é‡Šæ”¾ =9,
        [Description("已取走")]
        å·²å–èµ° =10,
        [Description("撤销")]
        æ’¤é”€ = 99
    }
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Common/StockEnum/StockStatusEmun.cs
@@ -40,6 +40,9 @@
        Lock,
    }
    /// <summary>
    /// åº“存状态: <br/>
    /// 1,组盘暂存<br/>
@@ -98,7 +101,10 @@
        [Description("盘点库存完成")]
        ç›˜ç‚¹åº“存完成 = 32,
        [Description("组盘撤销")]
        [Description("已清理")]
        å·²æ¸…理 = 33,
       [Description("组盘撤销")]
        ç»„盘撤销 = 99,
        [Description("入库撤销")]
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Core/Helper/SqlSugarHelper.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WIDESEA_Core.DB;
using WIDESEA_Core.Seed;
namespace WIDESEA_Core.Helper
{
    public class SqlSugarHelper
    {
        //多库情况下使用说明:
        //如果是固定多库可以传 new SqlSugarScope(List<ConnectionConfig>,db=>{}) æ–‡æ¡£ï¼šå¤šç§Ÿæˆ·
        //如果是不固定多库 å¯ä»¥çœ‹æ–‡æ¡£Saas分库
        //用单例模式
        public static SqlSugarScope DbWMS = new SqlSugarScope(new ConnectionConfig()
        {
            ConnectionString = DBContext.GetMainConnectionDb().Connection,
            //ConnectionString = AppSettings.app(MainDb.ConnectionStringWCS),
            DbType = DbType.SqlServer,//数据库类型
            IsAutoCloseConnection = true //不设成true要手动close
        },
      db =>
      {
      });
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_DTO/Allocate/AllocateDto.cs
@@ -146,6 +146,8 @@
        /// </summary>
        [JsonProperty("unit")]
        public string Unit { get; set; }
        public DateTime validDate { get; set; }
    }
    
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_DTO/Outbound/BatchOutBoundDto.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,169 @@
using SqlSugar;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WIDESEA_DTO.Outbound
{
    public class PalletLockInfoDto
    {
        public int Id { get; set; }
        public string OrderNo { get; set; }
        public string BatchNo { get; set; }
        public string MaterielCode { get; set; }
        public string CurrentBarcode { get; set; }
        public decimal AssignQuantity { get; set; }
        public decimal PickedQty { get; set; }
        public int Status { get; set; }
        public string LocationCode { get; set; }
        public string PalletCode { get; set; }
        public bool CanSplit { get; set; }
        public bool CanPick { get; set; }
    }
    #region è¯·æ±‚DTO
    public class ConfirmPickingRequest
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
        [Required(ErrorMessage = "条码不能为空")]
        public string Barcode { get; set; }
    }
    public class CancelPickingRequest
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
        [Required(ErrorMessage = "条码不能为空")]
        public string Barcode { get; set; }
        public int PickingHistoryId { get; set; }
    }
    public class CancelSplitRequest
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
        [Required(ErrorMessage = "新条码不能为空")]
        public string NewBarcode { get; set; }
    }
    public class ReturnStockRequest
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
    }
    public class CancelSplitDto
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
        [Required(ErrorMessage = "新条码不能为空")]
        public string NewBarcode { get; set; }
    }
    public class ReturnStockDto
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
    }
    public class PalletLocksDto
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
    }
    #endregion
    #region DTOç±»
    public class PalletPickedInfoDto
    {
        public int Id { get; set; }
        public string OrderNo { get; set; }
        public int OrderDetailId { get; set; }
        public string PalletCode { get; set; }
        public string Barcode { get; set; }
        public string MaterielCode { get; set; }
        public decimal PickedQty { get; set; }
        public DateTime PickTime { get; set; }
        public string Operator { get; set; }
        public string LocationCode { get; set; }
    }
    public class PalletStatusDto
    {
        public string OrderNo { get; set; }
        public string PalletCode { get; set; }
        public int Status { get; set; }
        public string StatusText { get; set; }
        public int TotalItems { get; set; }
        public int CompletedItems { get; set; }
        public int PendingItems { get; set; }
    }
    public class SplitPackageInfoDto
    {
        public string OrderNo { get; set; }
        public string PalletCode { get; set; }
        public string Barcode { get; set; }
        public string MaterielCode { get; set; }
        public decimal RemainQuantity { get; set; }
        public decimal AssignQuantity { get; set; }
        public decimal PickedQty { get; set; }
    }
    public class EmptyPalletRemovalDto
    {
        public string OrderNo { get; set; }
        public string PalletCode { get; set; }
        public DateTime RemovalTime { get; set; }
        public string Operator { get; set; }
        public int CompletedItemsCount { get; set; }
        public decimal TotalPickedQuantity { get; set; }
    }
    public class RemoveEmptyPalletDto
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
    }
    #endregion
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_DTO/Outbound/OutboundOrderAddDTO.cs
@@ -92,13 +92,16 @@
        /// </summary>
        public string orderNo { get; set; }
        public string documentsNO { get; set; }
        public string business_type { get; set; }
        public int status { get; set; }
        public string factoryArea { get; set; }
        public string Operator { get; set; }
        public List<FeedbackOutboundDetailsModel> details { get; set; }
         public List<FeedbackOutboundDetailsModel> details { get; set; }
    }
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_DTO/Outbound/OutboundOrderGetDTO.cs
@@ -2,6 +2,7 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@@ -37,8 +38,20 @@
    {
        public int OutStockLockInfoId { get; set; }
        public string MaterielCode { get; set; }
        public decimal SplitQuantity { get; set; }
        public string Operator { get; set; }
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
        [Required(ErrorMessage = "原条码不能为空")]
        public string OriginalBarcode { get; set; }
        [Range(0.001, double.MaxValue, ErrorMessage = "拆包数量必须大于0")]
        public decimal SplitQuantity { get; set; }
    }
    public class ConfirmPickingDto
    {
@@ -77,10 +90,7 @@
        public string OrderNo { get; set; }
    }
    public class CancelPickingRequest
    {
        public int PickingHistoryId { get; set; }
    }
    public class BackToStockRequest
    {
@@ -261,6 +271,29 @@
        public string Barcode { get; set; }
    }
    public class CancelSplitChainDto
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
        [Required(ErrorMessage = "起始条码不能为空")]
        public string StartBarcode { get; set; }
    }
    public class SplitPackageChainInfoRequestDto
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "条码不能为空")]
        public string Barcode { get; set; }
    }
    public class SplitPackageDto
    {
        public string OrderNo { get; set; }
@@ -295,4 +328,6 @@
        //public decimal SplitQuantity { get; set; }
        //public decimal RemainQuantity { get; set; }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_DTO/Stock/StockSelectViewDTO.cs
@@ -19,6 +19,14 @@
        public string LocationCode { get; set; }
        public string Barcode { get; set; }
        public string BatchNo { get; set; }
        public string SupplyCode { get; set; }
        public DateTime StockCreateDate { get; set; }
        public int StockId { get; set; }
        public int? OrderDetailId { get; set; }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_DTO/Task/WMSTaskDTO.cs
@@ -81,4 +81,15 @@
        public int[] taskIds { get; set; }
    }
    public class GenerateOutboundBatchTasksDto
    {
        public string orderNo { get; set; }
        public int orderDetailId { get; set; }
        public decimal batchQuantity { get; set; }
        public string outboundPlatform { get; set; }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_IBasicService/IMaterialUnitService.cs
@@ -20,6 +20,8 @@
        Task<Dictionary<string, MaterialWithUnitConversionResult>> BatchConvertPurchaseToStockAsync(List<BatchConversionRequest> requests);
        Task<MaterialWithUnitConversionResult> ConvertAsync(string materialCode, decimal quantity, string fromUnit, string toUnit);
        Task<MaterialWithUnitConversionResult> ConvertIssueToStockAsync(string materialCode, decimal quantity);
        Task<MaterialWithUnitConversionResult> ConvertFromToStockAsync(string materialCode, string fromUom, decimal quantity);
        Task<MaterialWithUnitConversionResult> ConvertPurchaseToStockAsync(string materialCode, decimal quantity);
        Task<decimal?> GetConversionRatioAsync(string materialCode, string fromUnit, string toUnit);
        Task<string> GetIssueUnitAsync(string materialCode);
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_IOutboundService/IOutStockLockInfoService.cs
@@ -26,7 +26,7 @@
        Task<List<Dt_OutStockLockInfo>> GetByPalletCode(string palletCode, int? status = null);
        Task<LockInfoDetailDto> GetLockInfoDetail(int lockInfoId);
        Dt_OutStockLockInfo GetOutStockLockInfo(Dt_OutboundOrder outboundOrder,Dt_OutboundOrderDetail outboundOrderDetail,Dt_StockInfo outStock, decimal assignQuantity, string barcode = null);
        Dt_OutStockLockInfo GetOutStockLockInfo(Dt_OutboundOrder outboundOrder,Dt_OutboundOrderDetail outboundOrderDetail,Dt_StockInfo outStock, decimal assignQuantity, string barcode = null, string outboundBatchNo = "");
        List<Dt_OutStockLockInfo> GetOutStockLockInfos(Dt_OutboundOrder outboundOrder, Dt_OutboundOrderDetail outboundOrderDetail, List<Dt_StockInfo> outStocks, int? taskNum = null);
        Task<List<Dt_OutStockLockInfo>> GetPalletLockInfos(string palletCode);
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_IOutboundService/IOutboundBatchPickingService.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
using WIDESEA_Core;
using WIDESEA_Core.BaseRepository;
using WIDESEA_DTO.Outbound;
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> CancelSplitPackageChain(string orderNo, string palletCode, string startBarcode);
        Task<List<Dt_SplitPackageRecord>> GetSplitPackageChain(string orderNo, string startBarcode);
        Task<string> FindRootBarcode(string orderNo, string startBarcode);
        Task<WebResponseContent> GetSplitPackageChainInfo(string orderNo, string barcode);
        Task<WebResponseContent> ConfirmBatchPicking(string orderNo, string palletCode, string barcode);
        Task<List<PalletLockInfoDto>> GetPalletLockInfos(string orderNo, string palletCode);
        Task<List<PalletPickedInfoDto>> GetPalletPickedList(string orderNo, string palletCode);
        Task<PalletStatusDto> GetPalletStatus(string orderNo, string palletCode);
        Task<SplitPackageInfoDto> GetSplitPackageInfo(string orderNo, string palletCode, string barcode);
        Task<WebResponseContent> ManualSplitPackage(string orderNo, string palletCode, string originalBarcode, decimal splitQuantity);
        Task<WebResponseContent> RemoveEmptyPallet(string orderNo, string palletCode);
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_IOutboundService/IOutboundOrderDetailService.cs
@@ -23,6 +23,6 @@
        WebResponseContent LockOutboundStockDataUpdate(List<Dt_StockInfo> stockInfos, List<Dt_OutboundOrderDetail> outboundOrderDetails, List<Dt_OutStockLockInfo> outStockLockInfos, List<Dt_LocationInfo> locationInfos, LocationStatusEnum locationStatus = LocationStatusEnum.Lock, List<Dt_Task>? tasks = null);
        (List<Dt_StockInfo>, Dt_OutboundOrderDetail, List<Dt_OutStockLockInfo>, List<Dt_LocationInfo>) AssignStockOutbound(Dt_OutboundOrderDetail outboundOrderDetail, List<StockSelectViewDTO> stockSelectViews);
        //List<Dt_OutboundOrderDetail> GetOutboundStockDataById(int id);
        Task<(List<Dt_StockInfo>, List<Dt_OutboundOrderDetail>, List<Dt_OutStockLockInfo>, List<Dt_LocationInfo>)> AssignStockForBatch(Dt_OutboundOrderDetail orderDetail, decimal batchQuantity, string batchNo);
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_IStockService/IStockInfoService.cs
@@ -13,7 +13,8 @@
        List<Dt_StockInfo> GetStockInfos(string materielCode, string lotNo, string supplyCode, List<string> locationCodes);
        List<Dt_StockInfo> GetUseableStocks(string materielCode, string batchNo,string supplyCode);
        Dt_StockInfo GetStockInfoByPalletCode(string palletCode);
        void AddMaterielGroup(Dt_StockInfo stockInfo);
        (List<Dt_StockInfo>, Dictionary<int, decimal>) GetOutboundStocks(List<Dt_StockInfo> stockInfos, string materielCode, decimal needQuantity, out decimal residueQuantity);
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_ITaskInfoService/ITaskService.cs
@@ -48,6 +48,8 @@
        WebResponseContent GenerateOutboundTask(int orderDetailId, List<StockSelectViewDTO> stockSelectViews);
        Task<WebResponseContent> GenerateOutboundBatchTasksAsync(int orderDetailId, decimal batchQuantity, string outStation);
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_InboundService/InboundOrderService.cs
@@ -96,8 +96,11 @@
                        item.Unit = purchaseToStockResult.Unit;
                        item.OrderQuantity = purchaseToStockResult.Quantity;
                    }
                    if (model.OrderType != InOrderTypeEnum.AllocatInbound.ObjToInt())
                    {
                        model.InboundOrderNo = CreateCodeByRule(nameof(RuleCodeEnum.InboundOrderRule));
                    }
                    model.InboundOrderNo = CreateCodeByRule(nameof(RuleCodeEnum.InboundOrderRule));
                    Db.InsertNav(model).Include(x => x.Details).ExecuteCommand();
                }
                return WebResponseContent.Instance.OK();
@@ -121,6 +124,10 @@
                    if (inboundOrder.Details == null || inboundOrder.Details.Count == 0)
                    {
                        return WebResponseContent.Instance.Error($"未找到入库单明细信息");
                    }
                    if (inboundOrder.OrderStatus != InOrderStatusEnum.未开始.ObjToInt())
                    {
                        return WebResponseContent.Instance.Error($"该订单状态不允许修改");
                    }
                    List<Dt_InboundOrderDetail> inboundOrderDetails = new List<Dt_InboundOrderDetail>();
                    List<Dt_InboundOrderDetail> updateInboundOrderDetails = new List<Dt_InboundOrderDetail>();
@@ -226,6 +233,10 @@
                    {
                        return WebResponseContent.Instance.Error($"未找到入库单明细信息");
                    }
                    if (inboundOrder.OrderStatus != InOrderStatusEnum.未开始.ObjToInt())
                    {
                        return WebResponseContent.Instance.Error($"该订单状态不允许删除");
                    }
                    //Db.DeleteNav(inboundOrder).Include(x => x.Details).ExecuteCommand();
                    _unitOfWorkManage.BeginTran();
                    //BaseDal.DeleteAndMoveIntoHty(inboundOrder, OperateTypeEnum.自动删除);
@@ -330,12 +341,12 @@
                if (!result2.Item1) return content = WebResponseContent.Instance.Error(result2.Item2);
                //  materielGroupDTO.WarehouseCode
                var code = _warehouseAreaRepository.Db.Queryable<Dt_WarehouseArea>().Where(x => x.Code == materielGroupDTO.WarehouseType).Select(x=>x.Code).First();
                if(string.IsNullOrEmpty(code))
                var code = _warehouseAreaRepository.Db.Queryable<Dt_WarehouseArea>().Where(x => x.Code == materielGroupDTO.WarehouseType).Select(x => x.Code).First();
                if (string.IsNullOrEmpty(code))
                {
                    return content = WebResponseContent.Instance.Error($"仓库中没有该{materielGroupDTO.WarehouseType}编号。");
                }
                Dt_InboundOrder inboundOrder = GetInboundOrder(materielGroupDTO.OrderNo);
@@ -357,10 +368,10 @@
                if (stockInfo == null)
                {
                    stockInfo = new Dt_StockInfo() { PalletType = (int)PalletTypeEnum.None,LocationType=materielGroupDTO.locationType.ObjToInt() };
                    stockInfo = new Dt_StockInfo() { PalletType = (int)PalletTypeEnum.None, LocationType = materielGroupDTO.locationType.ObjToInt() };
                    stockInfo.Details = new List<Dt_StockInfoDetail>();
                }
                foreach (var item in dbinboundOrderDetails)
                {
                    stockInfo.Details.Add(new Dt_StockInfoDetail
@@ -374,15 +385,15 @@
                        SupplyCode = item.SupplyCode,
                        WarehouseCode = materielGroupDTO.WarehouseType,
                        StockQuantity = item.OrderQuantity,
                        BarcodeQty=item.BarcodeQty,
                        BarcodeUnit=item.BarcodeUnit,
                        FactoryArea= inboundOrder.FactoryArea,
                        Status = 0,
                        BarcodeQty = item.BarcodeQty,
                        BarcodeUnit = item.BarcodeUnit,
                        FactoryArea = inboundOrder.FactoryArea,
                        Status = 0,
                        OrderNo = inboundOrder.InboundOrderNo,
                        BusinessType = inboundOrder.BusinessType,
                    });
                    item.ReceiptQuantity = item.BarcodeQty;
                    item.OrderDetailStatus = OrderDetailStatusEnum.Over.ObjToInt();
                    item.WarehouseCode = materielGroupDTO.WarehouseType;
@@ -429,7 +440,7 @@
            WebResponseContent content = new WebResponseContent();
            try
            {
            {
                (bool, string, object?) result2 = ModelValidate.ValidateModelData(materielGroupDTO);
                if (!result2.Item1) return content = WebResponseContent.Instance.Error(result2.Item2);
@@ -439,7 +450,8 @@
                    return content = WebResponseContent.Instance.Error($"区域中没有该{materielGroupDTO.WarehouseCode}编号。");
                }
                if(_stockRepository.QueryFirst(x=>x.PalletCode == materielGroupDTO.PalletCode)!=null){
                if (_stockRepository.QueryFirst(x => x.PalletCode == materielGroupDTO.PalletCode) != null)
                {
                    return WebResponseContent.Instance.Error("该托盘已经组过盘");
                }
@@ -458,7 +470,7 @@
                {
                    if (stockInfo == null)
                    {
                        stockInfo = new Dt_StockInfo() { PalletType = PalletTypeEnum.Empty.ObjToInt(), StockStatus = StockStatusEmun.组盘暂存.ObjToInt(), PalletCode = materielGroupDTO.PalletCode,LocationType= materielGroupDTO.WarehouseCode.ObjToInt() };
                        stockInfo = new Dt_StockInfo() { PalletType = PalletTypeEnum.Empty.ObjToInt(), StockStatus = StockStatusEmun.组盘暂存.ObjToInt(), PalletCode = materielGroupDTO.PalletCode, LocationType = materielGroupDTO.WarehouseCode.ObjToInt() };
                        stockInfo.Details = new List<Dt_StockInfoDetail>();
                    }
                    else
@@ -639,25 +651,31 @@
            {
                return WebResponseContent.Instance.Error("托盘号不能为空");
            }
           var stock= _stockRepository.Db.Queryable<Dt_StockInfo>().Includes(o=>o.Details).First(x => x.PalletCode == palletCode );
            var stock = _stockRepository.Db.Queryable<Dt_StockInfo>().Includes(o => o.Details).First(x => x.PalletCode == palletCode && x.StockStatus == (int)StockStatusEmun.组盘暂存);
            if (stock == null)
            {
                return WebResponseContent.Instance.Error($"未找到托盘号{palletCode}对应的库存记录");
            }
            if (stock.Details == null || !stock.Details.Any())
            {
                _stockRepository.DeleteData(stock);
                return WebResponseContent.Instance.OK();
            }
            //  èŽ·å–åº“å­˜è¯¦æƒ…å…³è”çš„æ‰€æœ‰å…¥åº“å•å·
            var relatedOrderNos = stock.Details.Select(d => d.OrderNo).First();
            //  æŸ¥è¯¢æ‰€æœ‰å…³è”的入库单(含详情)
            var inboundOrders = _inboundOrderRepository.Db.Queryable<Dt_InboundOrder>()
                .Includes(x => x.Details)
                .Where(x => relatedOrderNos==x.InboundOrderNo)
                .Includes(x => x.Details)
                .Where(x => relatedOrderNos == x.InboundOrderNo)
                .First();
            var barcodes = stock.Details.Select(d => d.Barcode).ToList();
                // åŒ¹é…åº“存条码对应的入库单明细
                var matchedInboundDetails = inboundOrders.Details
                    ?.Where(d => barcodes.Contains(d.Barcode))
                    .ToList();
            // åŒ¹é…åº“存条码对应的入库单明细
            var matchedInboundDetails = inboundOrders.Details
                ?.Where(d => barcodes.Contains(d.Barcode))
                .ToList();
            foreach (var detail in matchedInboundDetails)
            {
                detail.ReceiptQuantity = 0;
@@ -667,7 +685,8 @@
            _stockDetailRepository.DeleteData(stock.Details);
            _stockRepository.DeleteData(stock);
            return WebResponseContent.Instance.OK();
            return WebResponseContent.Instance.OK("托盘撤销成功");
        }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Model/Models/Allocate/Dt_AllocateOrderDetail.cs
@@ -120,5 +120,7 @@
        /// </summary>
        [SugarColumn(IsNullable = true, ColumnDescription = "回传MES")]
        public int ReturnToMESStatus { get; set; } = 0;
        [SugarColumn(IsNullable = true, ColumnDescription = "过期日期")]
        public DateTime ValidDate { get; set; }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Model/Models/Outbound/Dt_OutboundBatch.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WIDESEA_Core.DB.Models;
namespace WIDESEA_Model.Models.Outbound
{
    /// <summary>
    /// å‡ºåº“批次表
    /// </summary>
    [SugarTable("Dt_OutboundBatch")]
    public class Dt_OutboundBatch : BaseEntity
    {
        /// <summary>
        /// ä¸»é”®ID(自增)
        /// </summary>
        [SugarColumn(IsPrimaryKey = true, IsIdentity = true)] //
        public int Id { get; set; }
        /// <summary>
        /// æ‰¹æ¬¡å·
        /// </summary>
        [SugarColumn(ColumnName = "BatchNo", Length = 50, IsNullable = false)]
        public string BatchNo { get; set; }
        /// <summary>
        /// è®¢å•号
        /// </summary>
        [SugarColumn(ColumnName = "OrderNo", Length = 50, IsNullable = false)]
        public string OrderNo { get; set; }
        /// <summary>
        /// è®¢å•明细ID
        /// </summary>
        [SugarColumn(ColumnName = "OrderDetailId", IsNullable = false)]
        public int OrderDetailId { get; set; }
        /// <summary>
        /// æ‰¹æ¬¡åˆ†é…æ•°é‡
        /// </summary>
        [SugarColumn(ColumnName = "BatchQuantity",   IsNullable = false)] // ç²¾åº¦18,小数位2
        public decimal BatchQuantity { get; set; }
        /// <summary>
        /// å·²å®Œæˆæ•°é‡ï¼ˆé»˜è®¤0)
        /// </summary>
        [SugarColumn(ColumnName = "CompletedQuantity",   DefaultValue = "0")] // é»˜è®¤å€¼0
        public decimal CompletedQuantity { get; set; } = 0; // ä»£ç å±‚默认值,与数据库默认值一致
        /// <summary>
        /// æ‰¹æ¬¡çŠ¶æ€ï¼ˆé»˜è®¤0)
        /// </summary>
        [SugarColumn(ColumnName = "BatchStatus", DefaultValue = "0")]
        public int BatchStatus { get; set; } = 0;
        /// <summary>
        /// æ“ä½œäºº
        /// </summary>
        [SugarColumn(ColumnName = "Operator", Length = 50, IsNullable = true)] // å¯ç©º
        public string Operator { get; set; }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Model/Models/Outbound/Dt_OutboundLockInfo.cs
@@ -131,6 +131,13 @@
        [SugarColumn(IsNullable = false, Length = 50, ColumnDescription = "操作者")]
        public string Operator { get; set; }
        public decimal BarcodeQty { get; set; }
        public string BarcodeUnit { get; set; }
        public string OutboundBatchNo { get; set; }
        [Navigate(NavigateType.OneToOne, nameof(StockInfo))]//一对一 SchoolId是StudentA类里面的
        public Dt_StockInfo StockInfo { get; set; } //不能赋值只能是null
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Model/Models/Outbound/Dt_OutboundOrder.cs
@@ -104,7 +104,7 @@
        public string Operator { get; set; }
        /// <summary>
        /// å›žä¼ MES
        /// å›žä¼ MES 0未回传,1回传成功, 2回传失败
        /// </summary>
        [SugarColumn(IsNullable = true, ColumnDescription = "回传MES")]
        public int ReturnToMESStatus { get; set; } = 0;
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Model/Models/Outbound/Dt_OutboundOrderDetail.cs
@@ -143,9 +143,11 @@
        public decimal NeedOutQuantity => OrderQuantity - MoveQty;
        public decimal PickedQty { get; set; }
        /// <summary>
        /// è™šæ‹Ÿå‡ºå…¥åº“数量
        /// </summary>
        public decimal NoStockOutQty { get; set; }
        public string documentsNO { get; set; }
        public decimal AllocatedQuantity { get; set; }
        public string BatchAllocateStatus { get; set; }
    }
}
ÏîÄ¿´úÂë/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)]
@@ -115,6 +122,28 @@
       public decimal StockBeforeSplit { get; set; }
        public decimal AssignBeforeSplit { get; set; }
    }
    /// <summary>
    /// ç©ºç®±å–走记录表
    /// </summary>
    public class Dt_EmptyPalletRemoval
    {
        [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
        public int Id { get; set; }
        public string OrderNo { get; set; }
        public string PalletCode { get; set; }
        public DateTime RemovalTime { get; set; }
        public string Operator { get; set; }
        public int CompletedItemsCount { get; set; }
        public decimal TotalPickedQuantity { get; set; }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_OutboundService/OutStockLockInfoService.cs
@@ -43,7 +43,7 @@
            Dt_OutboundOrderDetail outboundOrderDetail,
            Dt_StockInfo outStock,
            decimal assignQuantity,
            string barcode = null)
            string barcode = null,string outboundBatchNo = "")
        {
            // èŽ·å–åº“å­˜æ˜Žç»†ä¿¡æ¯
            var stockDetails = outStock.Details
@@ -108,8 +108,11 @@
                MaterielCode = outboundOrderDetail.MaterielCode,
                BatchNo = firstAvailableDetail.BatchNo,
                Unit = firstAvailableDetail.Unit,
                BarcodeQty = firstAvailableDetail.BarcodeQty,
                BarcodeUnit = firstAvailableDetail.BarcodeUnit,
                FactoryArea = firstAvailableDetail.FactoryArea,
                lineNo = outboundOrderDetail.lineNo,
                OutboundBatchNo= outboundBatchNo
            };
        }
@@ -252,7 +255,7 @@
        public List<Dt_OutStockLockInfo> GetByOrderDetailId(int orderDetailId, OutLockStockStatusEnum? outStockStatus)
        {
            return BaseDal.QueryData(x => x.OrderDetailId == orderDetailId && outStockStatus == null ? true : x.Status == outStockStatus.ObjToInt());
            return BaseDal.QueryData(x => x.OrderDetailId == orderDetailId );
        }
    }
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundBatchPickingService.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1441 @@
using Microsoft.Extensions.Logging;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WIDESEA_BasicService;
using WIDESEA_Common.CommonEnum;
using WIDESEA_Common.OrderEnum;
using WIDESEA_Common.StockEnum;
using WIDESEA_Core;
using WIDESEA_Core.BaseRepository;
using WIDESEA_Core.BaseServices;
using WIDESEA_DTO.Outbound;
using WIDESEA_IAllocateService;
using WIDESEA_IBasicService;
using WIDESEA_IOutboundService;
using WIDESEA_IStockService;
using WIDESEA_Model.Models;
using WIDESEA_Model.Models.Basic;
using WIDESEA_Model.Models.Outbound;
using static WIDESEA_OutboundService.OutboundBatchPickingService;
namespace WIDESEA_OutboundService
{
    public class OutboundBatchPickingService : ServiceBase<Dt_PickingRecord, IRepository<Dt_PickingRecord>>, IOutboundBatchPickingService
    {
        private readonly IUnitOfWorkManage _unitOfWorkManage;
        public IRepository<Dt_PickingRecord> Repository => BaseDal;
        private readonly IStockInfoService _stockInfoService;
        private readonly IStockService _stockService;
        private readonly IOutStockLockInfoService _outStockLockInfoService;
        private readonly IStockInfoDetailService _stockInfoDetailService;
        private readonly ILocationInfoService _locationInfoService;
        private readonly IOutboundOrderDetailService _outboundOrderDetailService;
        private readonly IOutboundOrderService _outboundOrderService;
        private readonly ISplitPackageService _splitPackageService;
        private readonly IRepository<Dt_Task> _taskRepository;
        private readonly IESSApiService _eSSApiService;
        private readonly IInvokeMESService _invokeMESService;
        private readonly IDailySequenceService _dailySequenceService;
        private readonly IAllocateService _allocateService;
        private readonly IRepository<Dt_OutboundBatch> _outboundBatchRepository;
        private readonly ILogger<OutboundPickingService> _logger;
        private Dictionary<string, string> stations = new Dictionary<string, string>
        {
            {"2-1","2-9" },
            {"3-1","3-9" },
        };
        private Dictionary<string, string> movestations = new Dictionary<string, string>
        {
            {"2-1","2-5" },
            {"3-1","3-5" },
        };
        public OutboundBatchPickingService(IRepository<Dt_PickingRecord> BaseDal, IUnitOfWorkManage unitOfWorkManage, IStockInfoService stockInfoService, IStockService stockService,
            IOutStockLockInfoService outStockLockInfoService, IStockInfoDetailService stockInfoDetailService, ILocationInfoService locationInfoService,
            IOutboundOrderDetailService outboundOrderDetailService, ISplitPackageService splitPackageService, IOutboundOrderService outboundOrderService,
            IRepository<Dt_Task> taskRepository, IESSApiService eSSApiService, ILogger<OutboundPickingService> logger, IInvokeMESService invokeMESService, IDailySequenceService dailySequenceService, IAllocateService allocateService, IRepository<Dt_OutboundBatch> outboundBatchRepository) : base(BaseDal)
        {
            _unitOfWorkManage = unitOfWorkManage;
            _stockInfoService = stockInfoService;
            _stockService = stockService;
            _outStockLockInfoService = outStockLockInfoService;
            _stockInfoDetailService = stockInfoDetailService;
            _locationInfoService = locationInfoService;
            _outboundOrderDetailService = outboundOrderDetailService;
            _splitPackageService = splitPackageService;
            _outboundOrderService = outboundOrderService;
            _taskRepository = taskRepository;
            _eSSApiService = eSSApiService;
            _logger = logger;
            _invokeMESService = invokeMESService;
            _dailySequenceService = dailySequenceService;
            _allocateService = allocateService;
            _outboundBatchRepository = outboundBatchRepository;
        }
        // <summary>
        /// èŽ·å–æ‰˜ç›˜çš„é”å®šä¿¡æ¯
        /// </summary>
        public async Task<List<PalletLockInfoDto>> GetPalletLockInfos(string orderNo, string palletCode)
        {
            var lockInfos = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                  .Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode)
                  .Select(x => new
                  {
                      x.Id,
                      x.OrderNo,
                      x.BatchNo,
                      x.MaterielCode,
                      x.CurrentBarcode,
                      x.AssignQuantity,
                      x.PickedQty,
                      x.Status,
                      x.LocationCode,
                      x.PalletCode
                  }).ToListAsync();
            var lockInfoDtos = lockInfos.Select(x => new PalletLockInfoDto
            {
                Id = x.Id,
                OrderNo = x.OrderNo,
                BatchNo = x.BatchNo,
                MaterielCode = x.MaterielCode,
                CurrentBarcode = x.CurrentBarcode,
                AssignQuantity = x.AssignQuantity,
                PickedQty = x.PickedQty,
                Status = x.Status,
                LocationCode = x.LocationCode,
                PalletCode = x.PalletCode,
                CanSplit = (x.Status == (int)OutLockStockStatusEnum.出库中 && x.AssignQuantity - x.PickedQty > 0),
                CanPick = (x.Status == (int)OutLockStockStatusEnum.出库中 && x.PickedQty < x.AssignQuantity)
            }).ToList();
            return lockInfoDtos;
        }
        #region æŸ¥è¯¢æ–¹æ³•
        /// <summary>
        /// èŽ·å–æ‰˜ç›˜çš„å·²æ‹£é€‰åˆ—è¡¨
        /// </summary>
        public async Task<List<PalletPickedInfoDto>> GetPalletPickedList(string orderNo, string palletCode)
        {
            var pickedList = await Db.Queryable<Dt_PickingRecord>()
                .Where(x => x.OrderNo == orderNo &&
                           x.PalletCode == palletCode &&
                           !x.IsCancelled)
                .Select(x => new PalletPickedInfoDto
                {
                    Id = x.Id,
                    OrderNo = x.OrderNo,
                    OrderDetailId = x.OrderDetailId,
                    PalletCode = x.PalletCode,
                    Barcode = x.Barcode,
                    MaterielCode = x.MaterielCode,
                    PickedQty = x.PickQuantity,
                    PickTime = x.PickTime,
                    Operator = x.Operator,
                    LocationCode = x.LocationCode
                })
                .ToListAsync();
            return pickedList;
        }
        /// <summary>
        /// èŽ·å–æ‰˜ç›˜çŠ¶æ€
        /// </summary>
        public async Task<PalletStatusDto> GetPalletStatus(string orderNo, string palletCode)
        {
            // èŽ·å–æ‰˜ç›˜çš„é”å®šä¿¡æ¯
            var lockInfos = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode)
                .ToListAsync();
            if (!lockInfos.Any())
            {
                return new PalletStatusDto
                {
                    OrderNo = orderNo,
                    PalletCode = palletCode,
                    Status = (int)PalletStatusEnum.无任务,
                    StatusText = "无任务",
                    TotalItems = 0,
                    CompletedItems = 0,
                    PendingItems = 0
                };
            }
            var totalItems = lockInfos.Count;
            var completedItems = lockInfos.Count(x => x.Status == (int)OutLockStockStatusEnum.拣选完成);
            var pendingItems = lockInfos.Count(x => x.Status == (int)OutLockStockStatusEnum.出库中);
            var status = PalletStatusEnum.拣选中;
            if (pendingItems == 0 && completedItems > 0)
            {
                status = PalletStatusEnum.已完成;
            }
            else if (pendingItems > 0 && completedItems == 0)
            {
                status = PalletStatusEnum.未开始;
            }
            else if (pendingItems > 0 && completedItems > 0)
            {
                status = PalletStatusEnum.拣选中;
            }
            return new PalletStatusDto
            {
                OrderNo = orderNo,
                PalletCode = palletCode,
                Status = (int)status,
                StatusText = GetPalletStatusText(status),
                TotalItems = totalItems,
                CompletedItems = completedItems,
                PendingItems = pendingItems
            };
        }
        /// <summary>
        /// èŽ·å–æ‹†åŒ…ä¿¡æ¯
        /// </summary>
        public async Task<SplitPackageInfoDto> GetSplitPackageInfo(string orderNo, string palletCode, string barcode)
        {
            // æŸ¥æ‰¾é”å®šä¿¡æ¯
            var lockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(x => x.OrderNo == orderNo &&
                           x.PalletCode == palletCode &&
                           x.CurrentBarcode == barcode
                        //&& x.Status == (int)OutLockStockStatusEnum.出库中
                          )
                .FirstAsync();
            if (lockInfo == null)
                throw new Exception("未找到有效的锁定信息");
            // è®¡ç®—剩余可拆数量
            var remainQuantity = lockInfo.AssignQuantity - lockInfo.PickedQty;
            return new SplitPackageInfoDto
            {
                OrderNo = orderNo,
                PalletCode = palletCode,
                Barcode = barcode,
                MaterielCode = lockInfo.MaterielCode,
                RemainQuantity = remainQuantity,
                AssignQuantity = lockInfo.AssignQuantity,
                PickedQty = lockInfo.PickedQty
            };
        }
        #endregion
        #region å–走空箱逻辑
        /// <summary>
        /// å–走空箱 - æ¸…理已完成拣选的托盘数据
        /// </summary>
        public async Task<WebResponseContent> RemoveEmptyPallet(string orderNo, string palletCode)
        {
            try
            {
                _unitOfWorkManage.BeginTran();
                //  éªŒè¯æ‰˜ç›˜æ˜¯å¦å¯ä»¥å–走(必须全部完成拣选)
                var validationResult = await ValidateEmptyPalletRemoval(orderNo, palletCode);
                if (!validationResult.IsValid)
                    return WebResponseContent.Instance.Error(validationResult.ErrorMessage);
                var completedLocks = validationResult.Data;
                // æ¸…理锁定记录(标记为已完成)
                await CleanupCompletedLocks(completedLocks);
                // æ›´æ–°ç›¸å…³è®¢å•状态
                await UpdateOrderStatusAfterPalletRemoval(orderNo);
                //  è®°å½•操作历史
                // await RecordEmptyPalletRemoval(orderNo, palletCode, completedLocks);
                _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}");
            }
        }
        /// <summary>
        /// éªŒè¯ç©ºç®±å–走条件
        /// </summary>
        private async Task<ValidationResult<List<Dt_OutStockLockInfo>>> ValidateEmptyPalletRemoval(string orderNo, string palletCode)
        {
            // èŽ·å–æ‰˜ç›˜çš„æ‰€æœ‰é”å®šè®°å½•
            var lockInfos = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode)
                .ToListAsync();
            if (!lockInfos.Any())
                return ValidationResult<List<Dt_OutStockLockInfo>>.Error("该托盘没有锁定记录");
            // æ£€æŸ¥æ˜¯å¦æœ‰æœªå®Œæˆçš„锁定记录
            var unfinishedLocks = lockInfos.Where(x =>
                x.Status == (int)OutLockStockStatusEnum.出库中 ||
                x.Status == (int)OutLockStockStatusEnum.回库中).ToList();
            if (unfinishedLocks.Any())
            {
                var unfinishedCount = unfinishedLocks.Count;
                var unfinishedQty = unfinishedLocks.Sum(x => x.AssignQuantity - x.PickedQty);
                return ValidationResult<List<Dt_OutStockLockInfo>>.Error(
                    $"托盘还有{unfinishedCount}条未完成记录,剩余数量{unfinishedQty},不能取走空箱");
            }
            // èŽ·å–å·²å®Œæˆçš„é”å®šè®°å½•
            var completedLocks = lockInfos.Where(x =>
                x.Status == (int)OutLockStockStatusEnum.拣选完成).ToList();
            if (!completedLocks.Any())
                return ValidationResult<List<Dt_OutStockLockInfo>>.Error("该托盘没有已完成拣选的记录");
            return ValidationResult<List<Dt_OutStockLockInfo>>.Success(completedLocks);
        }
        /// <summary>
        /// æ¸…理已完成的锁定记录
        /// </summary>
        private async Task CleanupCompletedLocks(List<Dt_OutStockLockInfo> completedLocks)
        {
            foreach (var lockInfo in completedLocks)
            {
                // æ ‡è®°é”å®šè®°å½•为已取走(可以新增状态或直接删除,根据业务需求)
                // è¿™é‡Œæˆ‘们将其状态更新为"已取走",并记录取走时间
                lockInfo.Status = (int)OutLockStockStatusEnum.已取走;
                lockInfo.Operator = App.User.UserName;
                await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync();
                // åŒæ—¶æ¸…理对应的库存记录状态
                await CleanupStockInfo(lockInfo);
            }
        }
        /// <summary>
        /// æ¸…理库存信息
        /// </summary>
        private async Task CleanupStockInfo(Dt_OutStockLockInfo lockInfo)
        {
            var stockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .FirstAsync(x => x.Barcode == lockInfo.CurrentBarcode && x.StockId == lockInfo.StockId);
            if (stockDetail != null)
            {
                // å¦‚果库存已经出库完成,标记为已清理
                if (stockDetail.Status == (int)StockStatusEmun.出库完成)
                {
                    stockDetail.Status = (int)StockStatusEmun.已清理;
                    await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
                }
            }
        }
        /// <summary>
        /// æ›´æ–°è®¢å•状态
        /// </summary>
        private async Task UpdateOrderStatusAfterPalletRemoval(string orderNo)
        {
            // æ£€æŸ¥è®¢å•是否所有托盘都已完成
            var allLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(x => x.OrderNo == orderNo)
                .ToListAsync();
            var unfinishedPallets = allLocks
                .GroupBy(x => x.PalletCode)
                .Where(g => g.Any(x => x.Status == (int)OutLockStockStatusEnum.出库中 ||
                                      x.Status == (int)OutLockStockStatusEnum.回库中))
                .ToList();
            // å¦‚果没有未完成的托盘,更新订单状态为出库完成
            if (!unfinishedPallets.Any())
            {
                await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
                    .SetColumns(x => new Dt_OutboundOrder
                    {
                        OrderStatus = (int)OutOrderStatusEnum.出库完成,
                    })
                    .Where(x => x.OrderNo == orderNo)
                    .ExecuteCommandAsync();
            }
        }
        /// <summary>
        /// è®°å½•空箱取走历史
        /// </summary>
        private async Task RecordEmptyPalletRemoval(string orderNo, string palletCode, List<Dt_OutStockLockInfo> completedLocks)
        {
            var removalRecord = new Dt_EmptyPalletRemoval
            {
                OrderNo = orderNo,
                PalletCode = palletCode,
                RemovalTime = DateTime.Now,
                Operator = App.User.UserName,
                CompletedItemsCount = completedLocks.Count,
                TotalPickedQuantity = completedLocks.Sum(x => x.PickedQty)
            };
            await Db.Insertable(removalRecord).ExecuteCommandAsync();
        }
        #endregion
        #region è¾…助方法
        private string GetPalletStatusText(PalletStatusEnum status)
        {
            return status switch
            {
                PalletStatusEnum.未开始 => "未开始",
                PalletStatusEnum.拣选中 => "拣选中",
                PalletStatusEnum.已完成 => "已完成",
                PalletStatusEnum.无任务 => "无任务",
                _ => "未知"
            };
        }
        #endregion
        #region åˆ†æ‰¹åˆ†æ‹£
        /// <summary>
        /// åˆ†æ‰¹åˆ†æ‹£ç¡®è®¤
        /// </summary>
        public async Task<WebResponseContent> ConfirmBatchPicking(string orderNo, string palletCode, string barcode)
        {
            try
            {
                _unitOfWorkManage.BeginTran();
                // 1. éªŒè¯åˆ†æ‹£è¯·æ±‚
                var validationResult = await ValidatePickingRequest(orderNo, palletCode, barcode);
                if (!validationResult.IsValid)
                    return WebResponseContent.Instance.Error(validationResult.ErrorMessage);
                var (lockInfo, orderDetail, stockDetail, batch) = validationResult.Data;
                // ä½¿ç”¨é”å®šä¿¡æ¯çš„分配数量作为实际分拣数量
                var actualPickedQty = lockInfo.AssignQuantity;
                // 2. æ‰§è¡Œåˆ†æ‹£é€»è¾‘
                var pickingResult = await ExecutePickingLogic(lockInfo, orderDetail, stockDetail, actualPickedQty);
                // 3. æ›´æ–°æ‰¹æ¬¡å’Œè®¢å•数据
                await UpdateBatchAndOrderData(batch, orderDetail, actualPickedQty, orderNo);
                // 4. è®°å½•拣选历史
                await RecordPickingHistory(pickingResult, orderNo, palletCode);
                _unitOfWorkManage.CommitTran();
                return WebResponseContent.Instance.OK("分拣成功", new
                {
                    PickedQuantity = actualPickedQty,
                    Barcode = barcode,
                    MaterialCode = lockInfo.MaterielCode
                });
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                _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}");
            }
        }
        #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 å–消拆包
        #region å–消拆包 - ä¿®å¤ç‰ˆæœ¬
        /// <summary>
        /// å–消拆包 - æ”¯æŒå¤šæ¬¡æ‹†åŒ…的情况
        /// </summary>
        public async Task<WebResponseContent> CancelSplitPackage(string orderNo, string palletCode, string newBarcode)
        {
            try
            {
                _unitOfWorkManage.BeginTran();
                // 1. æŸ¥æ‰¾æ‹†åŒ…记录并验证
                var validationResult = await ValidateCancelSplitRequest(orderNo, palletCode, newBarcode);
                if (!validationResult.IsValid)
                    return WebResponseContent.Instance.Error(validationResult.ErrorMessage);
                var (splitRecord, newLockInfo, newStockDetail) = validationResult.Data;
                // 2. æŸ¥æ‰¾åŽŸå§‹é”å®šä¿¡æ¯
                var originalLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                    .FirstAsync(x => x.Id == splitRecord.OutStockLockInfoId);
                // 3. æ£€æŸ¥è¯¥æ¡ç æ˜¯å¦è¢«å†æ¬¡æ‹†åŒ…
                var childSplitRecords = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
                    .Where(x => x.OriginalBarcode == newBarcode && !x.IsReverted)
                    .ToListAsync();
                if (childSplitRecords.Any())
                {
                    return WebResponseContent.Instance.Error("该条码已被再次拆包,请先取消后续的拆包操作");
                }
                // 4. æ‰§è¡Œå–消拆包逻辑
                await ExecuteCancelSplitLogic(splitRecord, originalLockInfo, newLockInfo, newStockDetail);
                _unitOfWorkManage.CommitTran();
                return WebResponseContent.Instance.OK("取消拆包成功");
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                _logger.LogError($"取消拆包失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Barcode: {newBarcode}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error($"取消拆包失败:{ex.Message}");
            }
        }
        /// <summary>
        /// æ‰§è¡Œå–消拆包逻辑 - ä¿®å¤ç‰ˆæœ¬
        /// </summary>
        private async Task ExecuteCancelSplitLogic(Dt_SplitPackageRecord splitRecord,
            Dt_OutStockLockInfo originalLockInfo, Dt_OutStockLockInfo newLockInfo,
            Dt_StockInfoDetail newStockDetail)
        {
            // 1. æ¢å¤åŽŸé”å®šä¿¡æ¯
            // æ³¨æ„ï¼šè¿™é‡Œéœ€è¦ç´¯åŠ ï¼Œè€Œä¸æ˜¯ç®€å•çš„èµ‹å€¼ï¼Œå› ä¸ºå¯èƒ½æœ‰å¤šæ¬¡æ‹†åŒ…
            originalLockInfo.AssignQuantity += splitRecord.SplitQty;
            originalLockInfo.OrderQuantity += splitRecord.SplitQty;
            // å¦‚果原锁定信息的状态是拣选完成,需要重新设置为出库中
            if (originalLockInfo.Status == (int)OutLockStockStatusEnum.拣选完成)
            {
                originalLockInfo.Status = (int)OutLockStockStatusEnum.出库中;
            }
            await _outStockLockInfoService.Db.Updateable(originalLockInfo).ExecuteCommandAsync();
            // 2. æ¢å¤åŽŸåº“å­˜æ˜Žç»†
            var originalStock = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .FirstAsync(x => x.Barcode == splitRecord.OriginalBarcode && x.StockId == splitRecord.StockId);
            originalStock.StockQuantity += splitRecord.SplitQty;
            // å¦‚果原库存状态是出库完成,需要重新设置为出库锁定
            if (originalStock.Status == (int)StockStatusEmun.出库完成)
            {
                originalStock.Status = (int)StockStatusEmun.出库锁定;
            }
            await _stockInfoDetailService.Db.Updateable(originalStock).ExecuteCommandAsync();
            // 3. åˆ é™¤æ–°é”å®šä¿¡æ¯
            await _outStockLockInfoService.Db.Deleteable<Dt_OutStockLockInfo>()
                .Where(x => x.Id == newLockInfo.Id)
                .ExecuteCommandAsync();
            // 4. åˆ é™¤æ–°åº“存明细
            await _stockInfoDetailService.Db.Deleteable<Dt_StockInfoDetail>()
                .Where(x => x.Barcode == newLockInfo.CurrentBarcode)
                .ExecuteCommandAsync();
            // 5. æ ‡è®°æ‹†åŒ…记录为已撤销
            splitRecord.IsReverted = true;
            splitRecord.RevertTime = DateTime.Now;
            splitRecord.RevertOperator = App.User.UserName;
            await _splitPackageService.Db.Updateable(splitRecord).ExecuteCommandAsync();
            // 6. æ£€æŸ¥å¹¶æ›´æ–°æ‰¹æ¬¡å’Œè®¢å•状态
            await CheckAndUpdateBatchStatus(originalLockInfo.BatchNo);
            await CheckAndUpdateOrderStatus(originalLockInfo.OrderNo);
        }
        /// <summary>
        /// éªŒè¯å–消拆包请求 - å¢žå¼ºç‰ˆæœ¬
        /// </summary>
        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 childSplitRecords = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
                .Where(x => x.OriginalBarcode == newBarcode && !x.IsReverted)
                .ToListAsync();
            if (childSplitRecords.Any())
            {
                var childBarcodes = string.Join(", ", childSplitRecords.Select(x => x.NewBarcode));
                return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error(
                    $"该条码已被再次拆包,生成的新条码:{childBarcodes},请先取消后续拆包");
            }
            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 æ‰¹é‡å–消拆包链
        /// <summary>
        /// æ‰¹é‡å–消拆包链 - å–消某个条码及其所有后续拆包
        /// </summary>
        public async Task<WebResponseContent> CancelSplitPackageChain(string orderNo, string palletCode, string startBarcode)
        {
            try
            {
                _unitOfWorkManage.BeginTran();
                // 1. æŸ¥æ‰¾æ‰€æœ‰ç›¸å…³çš„æ‹†åŒ…记录(形成拆包链)
                var splitChain = await GetSplitPackageChain(orderNo, startBarcode);
                if (!splitChain.Any())
                    return WebResponseContent.Instance.Error("未找到拆包记录");
                // 2. æŒ‰æ‹†åŒ…顺序倒序取消(从最新的开始取消)
                var reversedChain = splitChain.OrderByDescending(x => x.SplitTime).ToList();
                foreach (var splitRecord in reversedChain)
                {
                    await CancelSingleSplitPackage(splitRecord, palletCode);
                }
                _unitOfWorkManage.CommitTran();
                return WebResponseContent.Instance.OK($"成功取消拆包链,共{reversedChain.Count}次拆包操作");
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                _logger.LogError($"取消拆包链失败 - OrderNo: {orderNo}, StartBarcode: {startBarcode}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error($"取消拆包链失败:{ex.Message}");
            }
        }
        /// <summary>
        /// èŽ·å–æ‹†åŒ…é“¾ - æŸ¥æ‰¾æŸä¸ªæ¡ç çš„æ‰€æœ‰æ‹†åŒ…记录(包括后续拆包)
        /// </summary>
        public async Task<List<Dt_SplitPackageRecord>> GetSplitPackageChain(string orderNo, string startBarcode)
        {
            var allSplitRecords = new List<Dt_SplitPackageRecord>();
            var visitedBarcodes = new HashSet<string>(); // é˜²æ­¢å¾ªçŽ¯å¼•ç”¨
            // ä½¿ç”¨é˜Ÿåˆ—进行广度优先搜索
            var queue = new Queue<string>();
            queue.Enqueue(startBarcode);
            visitedBarcodes.Add(startBarcode);
            while (queue.Count > 0)
            {
                var currentBarcode = queue.Dequeue();
                // æŸ¥æ‰¾ä»¥å½“前条码为原条码的所有拆包记录
                var splitRecords = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
                    .Where(x => x.OriginalBarcode == currentBarcode &&
                               x.OrderNo == orderNo &&
                               !x.IsReverted)
                    .ToListAsync();
                foreach (var record in splitRecords)
                {
                    // é¿å…é‡å¤å¤„理
                    if (!visitedBarcodes.Contains(record.NewBarcode))
                    {
                        allSplitRecords.Add(record);
                        queue.Enqueue(record.NewBarcode);
                        visitedBarcodes.Add(record.NewBarcode);
                    }
                }
            }
            return allSplitRecords;
        }
        /// <summary>
        /// å–消单个拆包记录
        /// </summary>
        private async Task CancelSingleSplitPackage(Dt_SplitPackageRecord splitRecord, string palletCode)
        {
            // æŸ¥æ‰¾ç›¸å…³æ•°æ®
            var newLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(x => x.CurrentBarcode == splitRecord.NewBarcode &&
                           x.PalletCode == palletCode)
                .FirstAsync();
            var newStockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .FirstAsync(x => x.Barcode == splitRecord.NewBarcode);
            var originalLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .FirstAsync(x => x.Id == splitRecord.OutStockLockInfoId);
            // æ‰§è¡Œå–消逻辑
            await ExecuteCancelSplitLogic(splitRecord, originalLockInfo, newLockInfo, newStockDetail);
        }
        #endregion
        #region æ‹†åŒ…信息查询增强
        /// <summary>
        /// èŽ·å–æ‹†åŒ…é“¾ä¿¡æ¯
        /// </summary>
        public async Task<WebResponseContent> GetSplitPackageChainInfo(string orderNo, string barcode)
        {
            try
            {
                var splitChain = await GetSplitPackageChain(orderNo, barcode);
                var chainInfo = new SplitPackageChainInfoDto
                {
                    OriginalBarcode = barcode,
                    TotalSplitTimes = splitChain.Count,
                    SplitChain = splitChain.Select(x => new SplitChainItemDto
                    {
                        SplitTime = x.SplitTime,
                        OriginalBarcode = x.OriginalBarcode,
                        NewBarcode = x.NewBarcode,
                        SplitQuantity = x.SplitQty,
                        Operator = x.Operator,
                        IsReverted = x.IsReverted
                    }).ToList()
                };
                return WebResponseContent.Instance.OK("获取成功", chainInfo);
            }
            catch (Exception ex)
            {
                _logger.LogError($"获取拆包链信息失败 - OrderNo: {orderNo}, Barcode: {barcode}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error("获取拆包链信息失败");
            }
        }
        /// <summary>
        /// æŸ¥æ‰¾æ ¹æ¡ç 
        /// </summary>
        public async Task<string> FindRootBarcode(string orderNo, string startBarcode)
        {
            var currentBarcode = startBarcode;
            var visited = new HashSet<string>();
            while (!string.IsNullOrEmpty(currentBarcode) && !visited.Contains(currentBarcode))
            {
                visited.Add(currentBarcode);
                // æŸ¥æ‰¾å½“前条码是否是由其他条码拆包而来
                var parentRecord = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
                    .Where(x => x.NewBarcode == currentBarcode &&
                               x.OrderNo == orderNo &&
                               !x.IsReverted)
                    .FirstAsync();
                if (parentRecord == null)
                {
                    // æ²¡æœ‰çˆ¶çº§æ‹†åŒ…记录,说明这是根条码
                    return currentBarcode;
                }
                currentBarcode = parentRecord.OriginalBarcode;
            }
            // å¦‚果出现循环引用,返回起始条码
            return startBarcode;
        }
        #endregion
        #region æ›´æ–°æ‰¹æ¬¡çŠ¶æ€æ£€æŸ¥
        /// <summary>
        /// æ£€æŸ¥å¹¶æ›´æ–°æ‰¹æ¬¡çŠ¶æ€
        /// </summary>
        private async Task CheckAndUpdateBatchStatus(string batchNo)
        {
            var batch = await _outboundBatchRepository.Db.Queryable<Dt_OutboundBatch>()
                .FirstAsync(x => x.BatchNo == batchNo);
            if (batch != null)
            {
                // é‡æ–°è®¡ç®—批次完成数量
                var batchLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                    .Where(x => x.BatchNo == batchNo)
                    .ToListAsync();
                var completedQuantity = batchLocks.Where(x => x.Status == (int)OutLockStockStatusEnum.拣选完成)
                                                 .Sum(x => x.PickedQty);
                batch.CompletedQuantity = completedQuantity;
                // æ›´æ–°æ‰¹æ¬¡çŠ¶æ€
                if (batch.CompletedQuantity >= batch.BatchQuantity)
                {
                    batch.BatchStatus = (int)BatchStatusEnum.已完成;
                }
                else if (batch.CompletedQuantity > 0)
                {
                    batch.BatchStatus = (int)BatchStatusEnum.执行中;
                }
                else
                {
                    batch.BatchStatus = (int)BatchStatusEnum.分配中;
                }
                await _outboundBatchRepository.Db.Updateable(batch).ExecuteCommandAsync();
            }
        }
        #endregion
        #region DTOç±»
        public class SplitPackageChainInfoDto
        {
            public string OriginalBarcode { get; set; }
            public string RootBarcode { get; set; } // æ–°å¢žï¼šæ ¹æ¡ç 
            public int TotalSplitTimes { get; set; }
            public string ChainType { get; set; } // "root" æˆ– "branch"
            public List<SplitChainItemDto> SplitChain { get; set; }
        }
        public class SplitChainItemDto
        {
            public DateTime SplitTime { get; set; }
            public string OriginalBarcode { get; set; }
            public string NewBarcode { get; set; }
            public decimal SplitQuantity { get; set; }
            public string Operator { get; set; }
            public bool IsReverted { get; set; }
        }
        #endregion
        #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.PalletCode == palletCode &&
                           x.CurrentBarcode == barcode &&
                           x.Status == (int)OutLockStockStatusEnum.出库中)
                .FirstAsync();
            if (lockInfo == null)
                return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.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);
            // éªŒè¯åº“存数量
            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.OutboundBatchNo);
            return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.Success((lockInfo, orderDetail, stockDetail, batch));
        }
        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));
        }
        #endregion
        #region æ ¸å¿ƒé€»è¾‘方法
        private async Task<PickingResult> ExecutePickingLogic(
            Dt_OutStockLockInfo lockInfo, Dt_OutboundOrderDetail orderDetail,
            Dt_StockInfoDetail stockDetail, decimal actualPickedQty)
        {
            // æ›´æ–°é”å®šä¿¡æ¯
            lockInfo.PickedQty += actualPickedQty;
            if (lockInfo.PickedQty >= lockInfo.AssignQuantity)
            {
                lockInfo.Status = (int)OutLockStockStatusEnum.拣选完成;
            }
            await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync();
            // æ›´æ–°åº“å­˜
            stockDetail.StockQuantity -= actualPickedQty;
            stockDetail.OutboundQuantity += actualPickedQty;
            if (stockDetail.StockQuantity <= 0)
            {
                stockDetail.Status = (int)StockStatusEmun.出库完成;
            }
            await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
            return new PickingResult
            {
                FinalLockInfo = lockInfo,
                ActualPickedQty = actualPickedQty
            };
        }
        private async Task<RevertPickingResult> RevertPickingData(Dt_PickingRecord pickingRecord)
        {
            // æ¢å¤é”å®šä¿¡æ¯
            var lockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .FirstAsync(x => x.Id == pickingRecord.OutStockLockId);
            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 == pickingRecord.Barcode);
            stockDetail.StockQuantity += pickingRecord.PickQuantity;
            stockDetail.OutboundQuantity -= pickingRecord.PickQuantity;
            stockDetail.Status = (int)StockStatusEmun.出库锁定;
            await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
            return new RevertPickingResult
            {
                LockInfo = lockInfo,
                StockDetail = stockDetail
            };
        }
        private async Task<SplitResultDto> ExecuteSplitLogic(Dt_OutStockLockInfo lockInfo, Dt_StockInfoDetail stockDetail,
            decimal splitQuantity, string palletCode)
        {
            // ç”Ÿæˆæ–°æ¡ç 
            string newBarcode = await GenerateNewBarcode();
            // åˆ›å»ºæ–°åº“存明细
            var newStockDetail = new Dt_StockInfoDetail
            {
                StockId = stockDetail.StockId,
                MaterielCode = stockDetail.MaterielCode,
                OrderNo = stockDetail.OrderNo,
                BatchNo = stockDetail.BatchNo,
                StockQuantity = splitQuantity,
                OutboundQuantity = 0,
                Barcode = newBarcode,
                Status = (int)StockStatusEmun.出库锁定,
                SupplyCode = stockDetail.SupplyCode,
                Unit = stockDetail.Unit,
                BarcodeQty=stockDetail.BarcodeQty,
                BarcodeUnit=stockDetail.BarcodeUnit,
                BusinessType=stockDetail.BusinessType,
                InboundOrderRowNo=stockDetail.InboundOrderRowNo,
            };
            await _stockInfoDetailService.Db.Insertable(newStockDetail).ExecuteCommandAsync();
            // æ›´æ–°åŽŸåº“å­˜æ˜Žç»†
            stockDetail.StockQuantity -= splitQuantity;
            await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
            var newLockInfo = new Dt_OutStockLockInfo
            {
                OrderNo = lockInfo.OrderNo,
                OrderDetailId = lockInfo.OrderDetailId,
                BatchNo = lockInfo.BatchNo,
                MaterielCode = lockInfo.MaterielCode,
                MaterielName = lockInfo.MaterielName,
                StockId = lockInfo.StockId,
                OrderQuantity = splitQuantity,
                //OriginalQuantity = quantity,
                AssignQuantity = splitQuantity,
                PickedQty = 0,
                LocationCode = lockInfo.LocationCode,
                PalletCode = lockInfo.PalletCode,
                TaskNum = lockInfo.TaskNum,
                Status = (int)OutLockStockStatusEnum.出库中,
                Unit = lockInfo.Unit,
                SupplyCode = lockInfo.SupplyCode,
                OrderType = lockInfo.OrderType,
                CurrentBarcode = newBarcode,
               // OriginalLockQuantity = quantity,
                IsSplitted = 1,
                ParentLockId = lockInfo.Id,
                Operator = App.User.UserName,
                FactoryArea = lockInfo.FactoryArea,
                lineNo = lockInfo.lineNo,
                WarehouseCode = lockInfo.WarehouseCode,
                BarcodeQty = lockInfo.BarcodeQty,
                BarcodeUnit = lockInfo.BarcodeUnit,
            };
            await _outStockLockInfoService.Db.Insertable(newLockInfo).ExecuteCommandAsync();
            // æ›´æ–°åŽŸé”å®šä¿¡æ¯
            lockInfo.AssignQuantity -= splitQuantity;
            lockInfo.OrderQuantity -= splitQuantity;
            await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync();
            // è®°å½•拆包历史
            await RecordSplitHistory(lockInfo, stockDetail, splitQuantity, newBarcode);
            return new SplitResultDto { NewBarcode = newBarcode };
        }
        private async Task ExecuteCancelSplitLogic(Dt_SplitPackageRecord splitRecord, Dt_OutStockLockInfo newLockInfo, Dt_StockInfoDetail newStockDetail)
        {
            // æ¢å¤åŽŸåº“å­˜
            var originalStock = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .FirstAsync(x => x.Barcode == splitRecord.OriginalBarcode && x.StockId == splitRecord.StockId);
            originalStock.StockQuantity += splitRecord.SplitQty;
            await _stockInfoDetailService.Db.Updateable(originalStock).ExecuteCommandAsync();
            // æ¢å¤åŽŸé”å®šä¿¡æ¯
            var originalLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .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 == newLockInfo.CurrentBarcode)
                .ExecuteCommandAsync();
            // åˆ é™¤æ–°é”å®šä¿¡æ¯
            await _outStockLockInfoService.Db.Deleteable<Dt_OutStockLockInfo>()
                .Where(x => x.Id == newLockInfo.Id)
                .ExecuteCommandAsync();
            // æ ‡è®°æ‹†åŒ…记录为已撤销
            splitRecord.IsReverted = true;
            splitRecord.RevertTime = DateTime.Now;
            splitRecord.RevertOperator = App.User.UserName;
            await _splitPackageService.Db.Updateable(splitRecord).ExecuteCommandAsync();
        }
        #endregion
        #region æ•°æ®æ›´æ–°æ–¹æ³•
        private async Task UpdateBatchAndOrderData(Dt_OutboundBatch batch, Dt_OutboundOrderDetail orderDetail, decimal pickedQty, string orderNo)
        {
            // æ›´æ–°æ‰¹æ¬¡å®Œæˆæ•°é‡
            batch.CompletedQuantity += pickedQty;
            if (batch.CompletedQuantity >= batch.BatchQuantity)
            {
                batch.BatchStatus = (int)BatchStatusEnum.已完成;
            }
            await _outboundBatchRepository.Db.Updateable(batch).ExecuteCommandAsync();
            // æ›´æ–°è®¢å•明细
            orderDetail.OverOutQuantity += pickedQty;
            orderDetail.AllocatedQuantity -= pickedQty;
            await _outboundOrderDetailService.Db.Updateable(orderDetail).ExecuteCommandAsync();
            // æ£€æŸ¥è®¢å•状态
            await CheckAndUpdateOrderStatus(orderNo);
        }
        private async Task RevertBatchAndOrderData(Dt_PickingRecord pickingRecord)
        {
            // æ¢å¤æ‰¹æ¬¡å®Œæˆæ•°é‡
            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)
        {
            // æ¢å¤åº“存状态
            var stockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .FirstAsync(x => x.Barcode == lockInfo.CurrentBarcode && x.StockId == lockInfo.StockId);
            if (stockDetail != null)
            {
                stockDetail.Status = (int)StockStatusEmun.入库完成;
                await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
            }
            // æ›´æ–°é”å®šè®°å½•状态为回库
            lockInfo.Status = (int)OutLockStockStatusEnum.已回库;
            await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync();
        }
        private async Task UpdateBatchStatusForReturn(string batchNo, List<Dt_OutStockLockInfo> returnedLocks)
        {
            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)
            {
                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)
        {
            var orderDetailGroups = returnedLocks.GroupBy(x => x.OrderDetailId);
            foreach (var group in orderDetailGroups)
            {
                var orderDetailId = group.Key;
                var returnedQty = group.Sum(x => x.AssignQuantity - x.PickedQty);
                await _outboundOrderDetailService.Db.Updateable<Dt_OutboundOrderDetail>()
                    .SetColumns(x => x.AllocatedQuantity == x.AllocatedQuantity - returnedQty)
                    .Where(x => x.Id == orderDetailId)
                    .ExecuteCommandAsync();
            }
        }
        #endregion
        #region è¾…助方法
        private async Task<string> GenerateNewBarcode()
        {
            var seq = await _dailySequenceService.GetNextSequenceAsync();
            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)
        {
            var splitHistory = new Dt_SplitPackageRecord
            {
                OrderNo = lockInfo.OrderNo,
                OutStockLockInfoId = lockInfo.Id,
                StockId = stockDetail.StockId,
                Operator = App.User.UserName,
                OriginalBarcode = stockDetail.Barcode,
                NewBarcode = newBarcode,
                SplitQty = splitQty,
                SplitTime = DateTime.Now,
                Status = (int)SplitPackageStatusEnum.已拆包
            };
            await _splitPackageService.Db.Insertable(splitHistory).ExecuteCommandAsync();
        }
        private async Task RecordPickingHistory(PickingResult result, string orderNo, string palletCode)
        {
            var pickingRecord = new Dt_PickingRecord
            {
                OrderNo = orderNo,
                // BatchNo = result.FinalLockInfo.BatchNo,
                OrderDetailId = result.FinalLockInfo.OrderDetailId,
                PalletCode = palletCode,
                Barcode = result.FinalLockInfo.CurrentBarcode,
                MaterielCode = result.FinalLockInfo.MaterielCode,
                PickQuantity = result.ActualPickedQty,
                PickTime = DateTime.Now,
                Operator = App.User.UserName,
                OutStockLockId = result.FinalLockInfo.Id,
                //  IsCancelled = false
            };
            await Db.Insertable(pickingRecord).ExecuteCommandAsync();
        }
        private async Task CheckAndUpdateOrderStatus(string orderNo)
        {
            var orderDetails = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>()
                .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);
            var orderStatus = allCompleted ? (int)OutOrderStatusEnum.出库完成 : (int)OutOrderStatusEnum.出库中;
            await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
                .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
    }
    // æ”¯æŒç±»
    public class SplitResultDto
    {
        public string NewBarcode { get; set; }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundOrderDetailService.cs
@@ -21,9 +21,9 @@
        private readonly IUnitOfWorkManage _unitOfWorkManage;
        public IRepository<Dt_OutboundOrderDetail> Repository => BaseDal;
        private readonly IStockService _stockService;
        private readonly IOutStockLockInfoService _outStockLockInfoService;
        private readonly ILocationInfoService _locationInfoService;
        private readonly IBasicService _basicService;
@@ -49,7 +49,8 @@
        /// <summary>
        /// åˆ†é…å‡ºåº“库存  æŒ‰å…ˆè¿›å…ˆå‡ºåŽŸåˆ™åˆ†é…
        /// </summary>
        public (List<Dt_StockInfo>, List<Dt_OutboundOrderDetail>, List<Dt_OutStockLockInfo>, List<Dt_LocationInfo>) AssignStockOutbound(List<Dt_OutboundOrderDetail> outboundOrderDetails)
        public (List<Dt_StockInfo>, List<Dt_OutboundOrderDetail>, List<Dt_OutStockLockInfo>, List<Dt_LocationInfo>)
       AssignStockOutbound(List<Dt_OutboundOrderDetail> outboundOrderDetails)
        {
            if (!outboundOrderDetails.Any())
            {
@@ -61,8 +62,14 @@
                throw new Exception("请勿同时操作多个单据明细");
            }
            var orderId = outboundOrderDetails.First().OrderId;
            var outboundOrder = _outboundOrderService.Db.Queryable<Dt_OutboundOrder>()
                .First(x => x.Id == outboundOrderDetails.First().OrderId);
                .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>();
@@ -70,14 +77,14 @@
            // æŒ‰ç‰©æ–™å’Œæ‰¹æ¬¡åˆ†ç»„处理
            var groupDetails = outboundOrderDetails
                .GroupBy(x => new { x.MaterielCode, x.BatchNo ,x.SupplyCode})
                .GroupBy(x => new { x.MaterielCode, x.BatchNo, x.SupplyCode })
                .Select(x => new
                {
                    MaterielCode = x.Key.MaterielCode,
                    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();
@@ -87,68 +94,121 @@
                var needQuantity = item.TotalNeedQuantity;
                // èŽ·å–å¯ç”¨åº“å­˜ï¼ˆæŒ‰å…ˆè¿›å…ˆå‡ºæŽ’åºï¼‰
                List<Dt_StockInfo> stockInfos = _stockService.StockInfoService.GetUseableStocks(item.MaterielCode, item.BatchNo,item.SupplyCode);
                List<Dt_StockInfo> stockInfos = _stockService.StockInfoService.GetUseableStocks(item.MaterielCode, item.BatchNo, item.SupplyCode);
                if (!stockInfos.Any())
                {
                    throw new Exception($"物料[{item.MaterielCode}]批次[{item.BatchNo}]未找到可分配库存");
                    throw new Exception($"物料[{item.MaterielCode}]批次[{item.BatchNo}]供应商[{item.SupplyCode}]未找到可分配库存");
                }
                // åˆ†é…åº“存(按先进先出)
                var (autoAssignStocks, stockAllocations) = _stockService.StockInfoService.GetOutboundStocks(
                    stockInfos, item.MaterielCode, needQuantity, out decimal residueQuantity);
                var (autoAssignStocks, stockAllocations) = _stockService.StockInfoService.GetOutboundStocks(stockInfos, item.MaterielCode, needQuantity, out decimal residueQuantity);
                if (residueQuantity > 0 && residueQuantity == needQuantity)
                // æ£€æŸ¥åˆ†é…ç»“果:如果完全无法分配(分配数量为0),则报错
                decimal allocatedQuantity = needQuantity - residueQuantity;
                if (allocatedQuantity <= 0)
                {
                    throw new Exception($"物料[{item.MaterielCode}]库存不足,需要{needQuantity},可用{needQuantity - residueQuantity}");
                    throw new Exception($"物料[{item.MaterielCode}]批次[{item.BatchNo}]库存不足,需要{needQuantity},但无法分配任何库存");
                }
                outStocks.AddRange(autoAssignStocks);
                // æŒ‰å…ˆè¿›å…ˆå‡ºåŽŸåˆ™åˆ†é…é”å®šæ•°é‡åˆ°å„ä¸ªæ˜Žç»†
                DistributeLockQuantityByFIFO(item.Details, autoAssignStocks, stockAllocations, outStockLockInfos, outboundOrder);
                var distributionResult = DistributeLockQuantityByFIFO(item.Details, autoAssignStocks, stockAllocations, outStockLockInfos, outboundOrder);
                if (!distributionResult.success)
                {
                    throw new Exception(distributionResult.message);
                }
                // æ›´æ–°å‡ºåº“单明细状态
                UpdateOrderDetailStatus(item.Details, allocatedQuantity, needQuantity);
            }
            locationInfos.AddRange(_locationInfoService.GetLocationInfos(outStocks.Select(x => x.LocationCode).Distinct().ToList()));
            if (outStocks.Any())
            {
                locationInfos.AddRange(_locationInfoService.GetLocationInfos(
                    outStocks.Select(x => x.LocationCode).Distinct().ToList()));
            }
            return (outStocks, outboundOrderDetails, outStockLockInfos, locationInfos);
        }
        /// <summary>
        /// æŒ‰å…ˆè¿›å…ˆå‡ºåŽŸåˆ™åˆ†é…é”å®šæ•°é‡åˆ°å„ä¸ªæ˜Žç»†
        /// æ£€æŸ¥è®¢å•是否允许重新分配
        /// </summary>
        private void DistributeLockQuantityByFIFO(
            List<Dt_OutboundOrderDetail> details,
            List<Dt_StockInfo> assignStocks,
            Dictionary<int, decimal> stockAllocations,
            List<Dt_OutStockLockInfo> outStockLockInfos,
            Dt_OutboundOrder outboundOrder)
        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,
           Dictionary<int, decimal> stockAllocations,
           List<Dt_OutStockLockInfo> outStockLockInfos,
           Dt_OutboundOrder outboundOrder)
        {
            // æŒ‰å…ˆè¿›å…ˆå‡ºæŽ’序出库单明细(Id小的优先)
            var sortedDetails = details
                .Where(d => d.OrderQuantity - d.OverOutQuantity - d.LockQuantity -d.MoveQty> 0) // åªå¤„理还需要分配的数量
                .Where(d => d.OrderQuantity - d.OverOutQuantity - d.LockQuantity - d.MoveQty > 0)
                .OrderBy(x => x.Id)
                .ToList();
            if (!sortedDetails.Any()) return;
            if (!sortedDetails.Any())
                return (true, "无需分配");
            // èŽ·å–æ‰€æœ‰åˆ†é…äº†åº“å­˜çš„æ˜Žç»†ï¼ŒæŒ‰å…ˆè¿›å…ˆå‡ºæŽ’åº
            var allocatedStockDetails = assignStocks
                .SelectMany(x => x.Details)
                .Where(x => stockAllocations.ContainsKey(x.Id))
                .Where(x => stockAllocations.ContainsKey(x.Id) && stockAllocations[x.Id] > 0)
                .OrderBy(x => x.CreateDate)
                .ThenBy(x => x.StockId)
                .ToList();
            if (!allocatedStockDetails.Any())
            {
                return (false, "没有可分配的库存明细");
            }
            decimal totalNeedQuantity = sortedDetails.Sum(d =>
                d.OrderQuantity - d.OverOutQuantity - d.LockQuantity - d.MoveQty);
            decimal allocatedQuantity = 0;
            // ä¸ºæ¯ä¸ªåº“存明细创建分配记录
            foreach (var stockDetail in allocatedStockDetails)
            {
                if (!stockAllocations.TryGetValue(stockDetail.Id, out decimal allocatedQuantity))
                if (!stockAllocations.TryGetValue(stockDetail.Id, out decimal allocatedQuantityForStock))
                    continue;
                if (allocatedQuantity <= 0) continue;
                if (allocatedQuantityForStock <= 0) continue;
                var stockInfo = assignStocks.First(x => x.Id == stockDetail.StockId);
                decimal remainingAllocate = allocatedQuantity;
                decimal remainingAllocate = allocatedQuantityForStock;
                // æŒ‰é¡ºåºåˆ†é…ç»™å„个出库单明细
                foreach (var detail in sortedDetails)
@@ -156,7 +216,7 @@
                    if (remainingAllocate <= 0) break;
                    // è®¡ç®—这个明细还需要分配的数量
                    var detailNeed = detail.OrderQuantity - detail.OverOutQuantity - detail.LockQuantity-detail.MoveQty;
                    var detailNeed = detail.OrderQuantity - detail.OverOutQuantity - detail.LockQuantity - detail.MoveQty;
                    if (detailNeed <= 0) continue;
                    // åˆ†é…æ•°é‡
@@ -165,7 +225,7 @@
                    // éªŒè¯æ¡ç æ˜¯å¦å­˜åœ¨
                    if (string.IsNullOrEmpty(stockDetail.Barcode))
                    {
                        throw new Exception($"库存明细ID[{stockDetail.Id}]的条码为空");
                        return (false, $"库存明细ID[{stockDetail.Id}]的条码为空");
                    }
                    // åˆ›å»ºå‡ºåº“锁定信息
@@ -176,38 +236,242 @@
                    // æ›´æ–°æ˜Žç»†çš„锁定数量
                    detail.LockQuantity += assignQuantity;
                    remainingAllocate -= assignQuantity;
                    allocatedQuantity += assignQuantity;
                }
                // å¦‚果还有剩余分配数量,重新分配或记录警告
                // å¦‚果还有剩余分配数量,记录警告
                if (remainingAllocate > 0)
                {
                    // é‡æ–°åˆ†é…ç»™å…¶ä»–需要分配的明细
                    foreach (var detail in sortedDetails)
                    _logger.LogWarning($"库存分配后仍有剩余数量未分配: {remainingAllocate}, æ¡ç : {stockDetail.Barcode}");
                }
            }
            // éªŒè¯æ˜¯å¦è‡³å°‘分配了一部分
            if (allocatedQuantity <= 0)
            {
                return (false, "库存分配失败,无法分配任何数量");
            }
            // è®°å½•分配结果
            if (allocatedQuantity < totalNeedQuantity)
            {
                _logger.LogWarning($"库存部分分配,需要{totalNeedQuantity},实际分配{allocatedQuantity}");
            }
            else
            {
                _logger.LogInformation($"库存完全分配,分配数量{allocatedQuantity}");
            }
            return (true, "分配成功");
        }
        /// <summary>
        /// ä¸ºåˆ†æ‰¹åˆ†é…åº“å­˜
        /// </summary>
        public async Task<(List<Dt_StockInfo>, List<Dt_OutboundOrderDetail>, List<Dt_OutStockLockInfo>, List<Dt_LocationInfo>)>
            AssignStockForBatch(Dt_OutboundOrderDetail orderDetail, decimal batchQuantity, string batchNo)
        {
            if (orderDetail == null)
            {
                throw new Exception("未找到出库单明细信息");
            }
            var outboundOrder = await _outboundOrderService.Db.Queryable<Dt_OutboundOrder>()
                .FirstAsync(x => x.Id == orderDetail.OrderId);
            List<Dt_StockInfo> outStocks = new List<Dt_StockInfo>();
            List<Dt_OutStockLockInfo> outStockLockInfos = new List<Dt_OutStockLockInfo>();
            List<Dt_LocationInfo> locationInfos = new List<Dt_LocationInfo>();
            // æŒ‰ç‰©æ–™å’Œæ‰¹æ¬¡åˆ†ç»„处理(这里只有一个明细)
            var groupDetails = new List<Dt_OutboundOrderDetail> { orderDetail }
                .GroupBy(x => new { x.MaterielCode, x.BatchNo, x.SupplyCode })
                .Select(x => new
                {
                    MaterielCode = x.Key.MaterielCode,
                    BatchNo = x.Key.BatchNo,
                    SupplyCode = x.Key.SupplyCode,
                    Details = x.ToList(),
                    TotalNeedQuantity = batchQuantity  // ä½¿ç”¨åˆ†æ‰¹æ•°é‡
                })
                .Where(x => x.TotalNeedQuantity > 0)
                .ToList();
            foreach (var item in groupDetails)
            {
                var needQuantity = item.TotalNeedQuantity;
                // èŽ·å–å¯ç”¨åº“å­˜ï¼ˆæŒ‰å…ˆè¿›å…ˆå‡ºæŽ’åºï¼‰
                List<Dt_StockInfo> stockInfos = _stockService.StockInfoService.GetUseableStocks(item.MaterielCode, item.BatchNo, item.SupplyCode);
                if (!stockInfos.Any())
                {
                    throw new Exception($"物料[{item.MaterielCode}]批次[{item.BatchNo}]未找到可分配库存");
                }
                // åˆ†é…åº“存(按先进先出)
                var (autoAssignStocks, stockAllocations) = _stockService.StockInfoService.GetOutboundStocks(stockInfos, item.MaterielCode, needQuantity, out decimal residueQuantity);
                // æ£€æŸ¥åˆ†é…ç»“æžœ
                decimal allocatedQuantity = needQuantity - residueQuantity;
                if (allocatedQuantity <= 0)
                {
                    throw new Exception($"物料[{item.MaterielCode}]批次[{item.BatchNo}]库存不足,需要{needQuantity},但无法分配任何库存");
                }
                outStocks.AddRange(autoAssignStocks);
                // æŒ‰å…ˆè¿›å…ˆå‡ºåŽŸåˆ™åˆ†é…é”å®šæ•°é‡åˆ°å„ä¸ªæ˜Žç»†
                var distributionResult = DistributeLockQuantityByFIFO(item.Details, autoAssignStocks, stockAllocations, outStockLockInfos, outboundOrder, batchNo);
                if (!distributionResult.success)
                {
                    throw new Exception(distributionResult.message);
                }
                // æ›´æ–°å‡ºåº“单明细状态
                UpdateOrderDetailStatus(item.Details, allocatedQuantity, needQuantity);
            }
            if (outStocks.Any())
            {
                locationInfos.AddRange(_locationInfoService.GetLocationInfos(
                  outStocks.Select(x => x.LocationCode).Distinct().ToList()));
            }
            return (outStocks, groupDetails.SelectMany(x => x.Details).ToList(), outStockLockInfos, locationInfos);
        }
        /// <summary>
        /// æŒ‰å…ˆè¿›å…ˆå‡ºåŽŸåˆ™åˆ†é…é”å®šæ•°é‡
        /// </summary>
        private (bool success, string message) DistributeLockQuantityByFIFO(
            List<Dt_OutboundOrderDetail> details,
            List<Dt_StockInfo> assignStocks,
            Dictionary<int, decimal> stockAllocations,
            List<Dt_OutStockLockInfo> outStockLockInfos,
            Dt_OutboundOrder outboundOrder,
            string batchNo)
        {
            var sortedDetails = details
                .Where(d => d.OrderQuantity - d.OverOutQuantity - d.AllocatedQuantity > 0)
                .OrderBy(x => x.Id)
                .ToList();
            if (!sortedDetails.Any())
                return (true, "无需分配");
            // èŽ·å–æ‰€æœ‰åˆ†é…äº†åº“å­˜çš„æ˜Žç»†ï¼ŒæŒ‰å…ˆè¿›å…ˆå‡ºæŽ’åº
            var allocatedStockDetails = assignStocks
                .SelectMany(x => x.Details)
                .Where(x => stockAllocations.ContainsKey(x.Id) && stockAllocations[x.Id] > 0)
                .OrderBy(x => x.CreateDate)
                .ThenBy(x => x.StockId)
                .ToList();
            if (!allocatedStockDetails.Any())
            {
                return (false, "没有可分配的库存明细");
            }
            decimal totalNeedQuantity = sortedDetails.Sum(d =>
                d.OrderQuantity - d.OverOutQuantity - d.AllocatedQuantity);
            decimal allocatedQuantity = 0;
            // ä¸ºæ¯ä¸ªåº“存明细创建分配记录
            foreach (var stockDetail in allocatedStockDetails)
            {
                if (!stockAllocations.TryGetValue(stockDetail.Id, out decimal allocatedQuantityForStock))
                    continue;
                if (allocatedQuantityForStock <= 0) continue;
                var stockInfo = assignStocks.First(x => x.Id == stockDetail.StockId);
                decimal remainingAllocate = allocatedQuantityForStock;
                // æŒ‰é¡ºåºåˆ†é…ç»™å„个出库单明细
                foreach (var detail in sortedDetails)
                {
                    if (remainingAllocate <= 0) break;
                    // è®¡ç®—这个明细还需要分配的数量
                    var detailNeed = detail.OrderQuantity - detail.OverOutQuantity - detail.AllocatedQuantity;
                    if (detailNeed <= 0) continue;
                    // åˆ†é…æ•°é‡
                    var assignQuantity = Math.Min(remainingAllocate, detailNeed);
                    // éªŒè¯æ¡ç æ˜¯å¦å­˜åœ¨
                    if (string.IsNullOrEmpty(stockDetail.Barcode))
                    {
                        if (remainingAllocate <= 0) break;
                        var detailNeed = detail.OrderQuantity - detail.OverOutQuantity - detail.LockQuantity;
                        if (detailNeed <= 0) continue;
                        var assignQuantity = Math.Min(remainingAllocate, detailNeed);
                        var lockInfo = _outStockLockInfoService.GetOutStockLockInfo(
                            outboundOrder, detail, stockInfo, assignQuantity, stockDetail.Barcode);
                        outStockLockInfos.Add(lockInfo);
                        detail.LockQuantity += assignQuantity;
                        remainingAllocate -= assignQuantity;
                        return (false, $"库存明细ID[{stockDetail.Id}]的条码为空");
                    }
                    // å¦‚果还有剩余,记录警告但不抛出异常
                    if (remainingAllocate > 0)
                    {
                        _logger.LogWarning($"库存分配后仍有剩余数量未分配: {remainingAllocate}, æ¡ç : {stockDetail.Barcode}");
                    }
                    // åˆ›å»ºå‡ºåº“锁定信息
                    var lockInfo = _outStockLockInfoService.GetOutStockLockInfo(
                        outboundOrder, detail, stockInfo, assignQuantity, stockDetail.Barcode, batchNo);
                    outStockLockInfos.Add(lockInfo);
                    // æ›´æ–°æ˜Žç»†çš„已分配数量
                    detail.AllocatedQuantity += assignQuantity;
                    remainingAllocate -= assignQuantity;
                    allocatedQuantity += assignQuantity;
                }
                // å¦‚果还有剩余分配数量,记录警告
                if (remainingAllocate > 0)
                {
                    _logger.LogWarning($"库存分配后仍有剩余数量未分配: {remainingAllocate}, æ¡ç : {stockDetail.Barcode}");
                }
            }
            // éªŒè¯æ˜¯å¦è‡³å°‘分配了一部分
            if (allocatedQuantity <= 0)
            {
                return (false, "库存分配失败,无法分配任何数量");
            }
            // è®°å½•分配结果
            if (allocatedQuantity < totalNeedQuantity)
            {
                _logger.LogWarning($"库存部分分配,需要{totalNeedQuantity},实际分配{allocatedQuantity}");
            }
            else
            {
                _logger.LogInformation($"库存完全分配,分配数量{allocatedQuantity}");
            }
            return (true, "分配成功");
        }
        private void UpdateOrderDetailStatus(List<Dt_OutboundOrderDetail> details,
    decimal allocatedQuantity, decimal needQuantity)
        {
            foreach (var detail in details)
            {
                var detailNeed = detail.OrderQuantity - detail.LockQuantity - detail.OverOutQuantity - detail.MoveQty;
                if (detailNeed <= 0)
                {
                    // è¯¥æ˜Žç»†å·²å®Œå…¨åˆ†é…
                    detail.OrderDetailStatus = OrderDetailStatusEnum.AssignOver.ObjToInt();
                }
                else if (allocatedQuantity < needQuantity)
                {
                    // æ•´ä½“部分分配,该明细可能还有需求
                    detail.OrderDetailStatus = OrderDetailStatusEnum.AssignOverPartial.ObjToInt();
                }
                else
                {
                    // æ•´ä½“完全分配
                    detail.OrderDetailStatus = OrderDetailStatusEnum.AssignOver.ObjToInt();
                }
            }
        }
        /// <summary>
        /// å‡ºåº“库存分配后,更新数据库数据
        /// </summary>
@@ -218,8 +482,8 @@
        /// <param name="locationStatus"></param>
        /// <param name="tasks"></param>
        /// <returns></returns>
        public WebResponseContent LockOutboundStockDataUpdate(List<Dt_StockInfo> stockInfos, List<Dt_OutboundOrderDetail> outboundOrderDetails,
            List<Dt_OutStockLockInfo> outStockLockInfos, List<Dt_LocationInfo> locationInfos,
        public WebResponseContent LockOutboundStockDataUpdate(List<Dt_StockInfo> stockInfos, List<Dt_OutboundOrderDetail> outboundOrderDetails,
            List<Dt_OutStockLockInfo> outStockLockInfos, List<Dt_LocationInfo> locationInfos,
            LocationStatusEnum locationStatus = LocationStatusEnum.Lock, List<Dt_Task>? tasks = null)
        {
            try
@@ -227,7 +491,7 @@
                // æ›´æ–°åº“存状态
                stockInfos.ForEach(x => x.StockStatus = (int)StockStatusEmun.出库锁定);
                _stockService.StockInfoService.Repository.UpdateData(stockInfos);
                // æ›´æ–°åº“存明细
                var stockDetails = stockInfos.SelectMany(x => x.Details).ToList();
                _stockService.StockInfoDetailService.Repository.UpdateData(stockDetails);
@@ -280,84 +544,244 @@
                        SearchParameters? searchParameters = searchParametersList.FirstOrDefault(x => x.Name == nameof(Dt_InboundOrderDetail.OrderId).FirstLetterToLower());
                        if (searchParameters != null)
                        {
                            sugarQueryable1 = sugarQueryable1.Where(x => x.OrderId== searchParameters.Value.ObjToInt());
                            var  dataList = sugarQueryable1.ToPageList(options.Page, options.Rows, ref totalCount);
                            sugarQueryable1 = sugarQueryable1.Where(x => x.OrderId == searchParameters.Value.ObjToInt());
                            var dataList = sugarQueryable1.ToPageList(options.Page, options.Rows, ref totalCount);
                            return new PageGridData<Dt_OutboundOrderDetail>(totalCount, dataList);
                        }
                    }
                }
            }
            return new PageGridData<Dt_OutboundOrderDetail> ();
            return new PageGridData<Dt_OutboundOrderDetail>();
        }
        public (List<Dt_StockInfo>, Dt_OutboundOrderDetail, List<Dt_OutStockLockInfo>, List<Dt_LocationInfo>) AssignStockOutbound(Dt_OutboundOrderDetail outboundOrderDetail, List<StockSelectViewDTO> stockSelectViews)
        public (List<Dt_StockInfo>, Dt_OutboundOrderDetail, List<Dt_OutStockLockInfo>, List<Dt_LocationInfo>)
       AssignStockOutbound(Dt_OutboundOrderDetail outboundOrderDetail, List<StockSelectViewDTO> stockSelectViews)
        {
            // éªŒè¯ç”¨æˆ·é€‰æ‹©
            (bool, string) checkResult = CheckSelectStockDeital(outboundOrderDetail, stockSelectViews);
            if (!checkResult.Item1) throw new Exception(checkResult.Item2);
            Dt_OutboundOrder outboundOrder = _outboundOrderService.Repository.QueryFirst(x => x.Id == outboundOrderDetail.OrderId);
            var originalNeedQuantity = outboundOrderDetail.OrderQuantity - outboundOrderDetail.LockQuantity;
            var originalNeedQuantity = outboundOrderDetail.OrderQuantity - outboundOrderDetail.LockQuantity - outboundOrderDetail.MoveQty;
            var needQuantity = originalNeedQuantity;
            List<Dt_StockInfo> outStocks = new List<Dt_StockInfo>();
            List<Dt_OutStockLockInfo> outStockLockInfos = new List<Dt_OutStockLockInfo>();
            List<Dt_StockInfo> outStocks = _stockService.StockInfoService.GetStockInfosByPalletCodes(stockSelectViews.Select(x => x.PalletCode).ToList());
            var assignQuantity =0m;
            outStocks.ForEach(x =>
            {
                x.Details.ForEach(v =>
                {
                    assignQuantity += v.StockQuantity - v.OutboundQuantity;
                });
            });
            decimal remainingNeedQuantity = originalNeedQuantity;
            decimal totalAssignedFromUserSelection = 0;
            outboundOrderDetail.LockQuantity += assignQuantity;
            outStocks.ForEach(x =>
            // æŒ‰å…ˆè¿›å…ˆå‡ºæŽ’序用户选择的库存
            var userSelectedStocks = _stockService.StockInfoService.GetStockInfosByPalletCodes(
                stockSelectViews.Select(x => x.PalletCode).ToList());
            var sortedUserSelectedStocks = userSelectedStocks
                .OrderBy(x => x.CreateDate)
                .ToList();
            // åˆ†é…ç”¨æˆ·é€‰æ‹©çš„库存
            foreach (var stock in sortedUserSelectedStocks)
            {
                x.Details.ForEach(v =>
                if (remainingNeedQuantity <= 0) break;
                // èŽ·å–ç”¨æˆ·å¯¹è¯¥æ‰˜ç›˜çš„é€‰æ‹©æ•°é‡
                var userSelection = stockSelectViews.FirstOrDefault(x => x.PalletCode == stock.PalletCode);
                if (userSelection == null) continue;
                // è®¡ç®—该托盘实际可用数量
                var availableQuantity = CalculateAvailableQuantity(stock, outboundOrderDetail.MaterielCode,
                    outboundOrderDetail.BatchNo, outboundOrderDetail.SupplyCode);
                // ç¡®å®šåˆ†é…æ•°é‡ï¼šå–用户选择数量、可用数量和剩余需求的最小值
                var assignQuantity = Math.Min(
                    Math.Min(userSelection.UseableQuantity, availableQuantity),
                    remainingNeedQuantity);
                if (assignQuantity <= 0) continue;
                // æ‰§è¡Œåˆ†é…
                var actualAssigned = AssignStockQuantity(stock, outboundOrderDetail, assignQuantity);
                if (actualAssigned > 0)
                {
                    v.OutboundQuantity = v.StockQuantity;
                });
            });
            needQuantity -= assignQuantity;
            if (outboundOrderDetail.OrderQuantity > outboundOrderDetail.LockQuantity)
            {
                List<Dt_StockInfo> stockInfos = _stockService.StockInfoService.GetUseableStocks(outboundOrderDetail.MaterielCode, outboundOrderDetail.BatchNo,"");
                stockInfos = stockInfos.Where(x => !stockSelectViews.Select(v => v.PalletCode).Contains(x.PalletCode)).ToList();
                var (autoAssignStocks, stockAllocations)   = _stockService.StockInfoService.GetOutboundStocks(stockInfos, outboundOrderDetail.MaterielCode, needQuantity, out decimal residueQuantity);
                outboundOrderDetail.LockQuantity += needQuantity - residueQuantity;
                outStocks.AddRange(autoAssignStocks);
                outboundOrderDetail.OrderDetailStatus = OrderDetailStatusEnum.AssignOver.ObjToInt();
                if (residueQuantity > 0)
                {
                    outboundOrderDetail.OrderDetailStatus = OrderDetailStatusEnum.AssignOverPartial.ObjToInt();
                    outStocks.Add(stock);
                    totalAssignedFromUserSelection += actualAssigned;
                    remainingNeedQuantity -= actualAssigned;
                    // åˆ›å»ºé”å®šè®°å½•
                    var lockInfo = CreateOutStockLockInfo(outboundOrder, outboundOrderDetail, stock, actualAssigned);
                    outStockLockInfos.Add(lockInfo);
                }
            }
            List<Dt_OutStockLockInfo> outStockLockInfos = _outStockLockInfoService.GetOutStockLockInfos(outboundOrder, outboundOrderDetail, outStocks);
            // æ£€æŸ¥ç”¨æˆ·é€‰æ‹©æ˜¯å¦è‡³å°‘分配了一部分
            if (totalAssignedFromUserSelection <= 0)
            {
                throw new Exception("用户选择的库存无法分配任何数量");
            }
            // å¦‚果用户选择的库存不够,自动分配剩余部分
            decimal autoAssignedQuantity = 0;
            if (remainingNeedQuantity > 0)
            {
                List<Dt_StockInfo> autoStocks = _stockService.StockInfoService.GetUseableStocks(
                    outboundOrderDetail.MaterielCode, outboundOrderDetail.BatchNo, "");
                // æŽ’除用户已选择的托盘
                autoStocks = autoStocks
                    .Where(x => !stockSelectViews.Select(v => v.PalletCode).Contains(x.PalletCode))
                    .ToList();
                var (autoAssignStocks, stockAllocations) = _stockService.StockInfoService.GetOutboundStocks(
                    autoStocks, outboundOrderDetail.MaterielCode, remainingNeedQuantity, out decimal residueQuantity);
                // æ£€æŸ¥è‡ªåŠ¨åˆ†é…ç»“æžœ
                autoAssignedQuantity = remainingNeedQuantity - residueQuantity;
                if (autoAssignedQuantity <= 0 && remainingNeedQuantity > 0)
                {
                    // éƒ¨åˆ†åˆ†é…æ˜¯å¯ä»¥æŽ¥å—的,记录警告但不报错
                    _logger.LogWarning($"自动分配失败,剩余需求{remainingNeedQuantity}无法满足");
                }
                else if (autoAssignedQuantity > 0)
                {
                    outStocks.AddRange(autoAssignStocks);
                    // ä¸ºè‡ªåŠ¨åˆ†é…çš„åº“å­˜åˆ›å»ºé”å®šè®°å½•
                    var autoLockInfos = CreateLockInfosForAutoAssign(outboundOrder, outboundOrderDetail, autoAssignStocks, stockAllocations);
                    outStockLockInfos.AddRange(autoLockInfos);
                }
            }
            // æ›´æ–°é”å®šæ•°é‡
            outboundOrderDetail.LockQuantity += totalAssignedFromUserSelection + autoAssignedQuantity;
            // æ›´æ–°çŠ¶æ€
            UpdateOrderDetailStatus(outboundOrderDetail, remainingNeedQuantity - autoAssignedQuantity);
            List<Dt_LocationInfo> locationInfos = _locationInfoService.GetLocationInfos(outStocks.Select(x => x.LocationCode).ToList());
            return (outStocks, outboundOrderDetail, outStockLockInfos, locationInfos);
        }
        private (bool, string) CheckSelectStockDeital(Dt_OutboundOrderDetail outboundOrderDetail, List<StockSelectViewDTO> stockSelectViews)
        // è¾…助方法
        private decimal CalculateAvailableQuantity(Dt_StockInfo stock, string materielCode, string batchNo, string supplyCode)
        {
            if (outboundOrderDetail == null)
            {
                return (false, "未找到出库单明细信息");
            }
            if (outboundOrderDetail.OrderDetailStatus != OrderDetailStatusEnum.New.ObjToInt() && outboundOrderDetail.OrderDetailStatus != OrderDetailStatusEnum.AssignOverPartial.ObjToInt())
            {
                return (false, "该明细不可操作");
            }
            if (stockSelectViews.Sum(x => x.UseableQuantity) > outboundOrderDetail.OrderQuantity - outboundOrderDetail.LockQuantity)
            {
                return (false, "选择数量超出单据数量");
            }
            return (true, "成功");
            var relevantDetails = stock.Details
                .Where(d => d.MaterielCode == materielCode &&
                           (string.IsNullOrEmpty(batchNo) || d.BatchNo == batchNo) &&
                           (string.IsNullOrEmpty(supplyCode) || d.SupplyCode == supplyCode))
                .ToList();
            return relevantDetails.Sum(d => d.StockQuantity - d.OutboundQuantity);
        }
        private decimal AssignStockQuantity(Dt_StockInfo stock, Dt_OutboundOrderDetail detail, decimal assignQuantity)
        {
            decimal remainingAssign = assignQuantity;
            // æŒ‰å…ˆè¿›å…ˆå‡ºåˆ†é…åº“存明细
            var sortedDetails = stock.Details
                .Where(d => d.MaterielCode == detail.MaterielCode &&
                           d.BatchNo == detail.BatchNo &&
                           (d.StockQuantity - d.OutboundQuantity) > 0)
                .OrderBy(d => d.CreateDate)
                .ToList();
            foreach (var stockDetail in sortedDetails)
            {
                if (remainingAssign <= 0) break;
                var available = stockDetail.StockQuantity - stockDetail.OutboundQuantity;
                var assign = Math.Min(available, remainingAssign);
                stockDetail.OutboundQuantity += assign;
                remainingAssign -= assign;
            }
            return assignQuantity - remainingAssign; // è¿”回实际分配数量
        }
        private Dt_OutStockLockInfo CreateOutStockLockInfo(Dt_OutboundOrder outboundOrder, Dt_OutboundOrderDetail detail,
            Dt_StockInfo stock, decimal quantity)
        {
            var barcode = stock.Details
                .Where(d => !string.IsNullOrEmpty(d.Barcode))
                .Select(d => d.Barcode)
                .FirstOrDefault();
            return _outStockLockInfoService.GetOutStockLockInfo(outboundOrder, detail, stock, quantity, barcode);
        }
        private List<Dt_OutStockLockInfo> CreateLockInfosForAutoAssign(Dt_OutboundOrder outboundOrder,
            Dt_OutboundOrderDetail detail, List<Dt_StockInfo> stocks, Dictionary<int, decimal> allocations)
        {
            var lockInfos = new List<Dt_OutStockLockInfo>();
            foreach (var stock in stocks.OrderBy(x => x.CreateDate))
            {
                if (allocations.TryGetValue(stock.Id, out decimal quantity) && quantity > 0)
                {
                    var lockInfo = CreateOutStockLockInfo(outboundOrder, detail, stock, quantity);
                    lockInfos.Add(lockInfo);
                }
            }
            return lockInfos;
        }
        private void UpdateOrderDetailStatus(Dt_OutboundOrderDetail detail, decimal remainingQuantity)
        {
            if (remainingQuantity <= 0)
            {
                detail.OrderDetailStatus = OrderDetailStatusEnum.AssignOver.ObjToInt();
            }
            else
            {
                detail.OrderDetailStatus = OrderDetailStatusEnum.AssignOverPartial.ObjToInt();
            }
        }
        private (bool, string) CheckSelectStockDeital(Dt_OutboundOrderDetail outboundOrderDetail, List<StockSelectViewDTO> stockSelectViews)
        {
            var needQuantity = outboundOrderDetail.OrderQuantity - outboundOrderDetail.LockQuantity - outboundOrderDetail.MoveQty;
            if (needQuantity <= 0)
            {
                return (false, "出库单明细无需分配库存");
            }
            // æ£€æŸ¥æ€»é€‰æ‹©æ•°é‡æ˜¯å¦å¤§äºŽ0
            var totalSelected = stockSelectViews.Sum(x => x.UseableQuantity);
            if (totalSelected <= 0)
            {
                return (false, "用户选择的库存数量必须大于0");
            }
            // æ£€æŸ¥æ¯ä¸ªæ‰˜ç›˜çš„可用数量
            foreach (var selection in stockSelectViews)
            {
                if (selection.UseableQuantity <= 0)
                {
                    return (false, $"托盘[{selection.PalletCode}]的选择数量必须大于0");
                }
                var stock = _stockService.StockInfoService.GetStockInfoByPalletCode(selection.PalletCode);
                if (stock == null)
                {
                    return (false, $"托盘[{selection.PalletCode}]不存在");
                }
                var available = CalculateAvailableQuantity(stock, outboundOrderDetail.MaterielCode,
                    outboundOrderDetail.BatchNo, outboundOrderDetail.SupplyCode);
                if (available <= 0)
                {
                    return (false, $"托盘[{selection.PalletCode}]没有可用库存");
                }
            }
            return (true, "验证通过");
        }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundOrderService.cs
@@ -3,6 +3,8 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using SqlSugar;
using SqlSugar.Extensions;
using WIDESEA_Common.OrderEnum;
using WIDESEA_Core;
using WIDESEA_Core.BaseRepository;
using WIDESEA_Core.BaseServices;
@@ -63,15 +65,17 @@
                }
                foreach (var item in model.Details)
                {
                    var issueoStockResult = await _materialUnitService.ConvertIssueToStockAsync(item.MaterielCode, item.BarcodeQty);
                    var issueoStockResult = await _materialUnitService.ConvertFromToStockAsync(item.MaterielCode,item.BarcodeUnit, item.BarcodeQty);
                    item.Unit = issueoStockResult.Unit;
                    item.OrderQuantity = issueoStockResult.Quantity;
                    var moveissueoStockResult = await _materialUnitService.ConvertIssueToStockAsync(item.MaterielCode, item.BarcodeMoveQty);
                    var moveissueoStockResult = await _materialUnitService.ConvertFromToStockAsync(item.MaterielCode, item.BarcodeUnit, item.BarcodeMoveQty);
                    item.MoveQty = moveissueoStockResult.Quantity;
                }
                model.OrderNo = CreateCodeByRule(nameof(RuleCodeEnum.OutboundOrderRule));
                if (model.OrderType != InOrderTypeEnum.AllocatOutbound.ObjToInt() || model.OrderType != InOrderTypeEnum.InternalAllocat.ObjToInt())
                {
                    model.OrderNo = CreateCodeByRule(nameof(RuleCodeEnum.OutboundOrderRule));
                }
                Db.InsertNav(model).Include(x => x.Details).ExecuteCommand();
                return WebResponseContent.Instance.OK();
@@ -94,6 +98,10 @@
                if (outboundOrder.Details == null || outboundOrder.Details.Count == 0)
                {
                    return WebResponseContent.Instance.Error($"未找到出库单明细信息");
                }
                if (outboundOrder.OrderStatus != OutOrderStatusEnum.未开始.ObjToInt())
                {
                    return WebResponseContent.Instance.Error($"该订单状态不允许修改");
                }
                List<Dt_OutboundOrderDetail> outboundOrderDetails = new List<Dt_OutboundOrderDetail>();
                List<Dt_OutboundOrderDetail> updateoutboundOrderDetails = new List<Dt_OutboundOrderDetail>();
@@ -119,10 +127,10 @@
                            BarcodeQty = item.OrderQuantity,
                            BarcodeUnit = item.Unit,
                        };
                        var issueoStockResult = await _materialUnitService.ConvertIssueToStockAsync(item.MaterielCode, item.BarcodeQty);
                        var issueoStockResult = await _materialUnitService.ConvertFromToStockAsync(item.MaterielCode,item.BarcodeUnit, item.BarcodeQty);
                        item.Unit = issueoStockResult.Unit;
                        item.OrderQuantity = issueoStockResult.Quantity;
                        var moveissueoStockResult = await _materialUnitService.ConvertIssueToStockAsync(item.MaterielCode, item.BarcodeMoveQty);
                        var moveissueoStockResult = await _materialUnitService.ConvertFromToStockAsync(item.MaterielCode, item.BarcodeUnit, item.BarcodeMoveQty);
                        item.MoveQty = moveissueoStockResult.Quantity;
                        outboundOrderDetails.Add(outboundOrderDetail);
@@ -140,10 +148,10 @@
                        outboundOrderDetail.BarcodeMoveQty = item.MoveQty;
                        outboundOrderDetail.BarcodeQty = item.OrderQuantity;
                        outboundOrderDetail.BarcodeUnit = item.Unit;
                        var issueoStockResult = await _materialUnitService.ConvertIssueToStockAsync(item.MaterielCode, item.BarcodeQty);
                        var issueoStockResult = await _materialUnitService.ConvertFromToStockAsync(item.MaterielCode, item.BarcodeUnit, item.BarcodeQty);
                        outboundOrderDetail.Unit = issueoStockResult.Unit;
                        outboundOrderDetail.OrderQuantity = issueoStockResult.Quantity;
                        var moveissueoStockResult = await _materialUnitService.ConvertIssueToStockAsync(item.MaterielCode, item.BarcodeMoveQty);
                        var moveissueoStockResult = await _materialUnitService.ConvertFromToStockAsync(item.MaterielCode, item.BarcodeUnit, item.BarcodeMoveQty);
                        outboundOrderDetail.MoveQty = moveissueoStockResult.Quantity;
@@ -195,7 +203,10 @@
                {
                    return WebResponseContent.Instance.Error($"未找到出库单明细信息");
                }
                if (outboundOrder.OrderStatus != OutOrderStatusEnum.未开始.ObjToInt())
                {
                    return WebResponseContent.Instance.Error($"该订单状态不允许删除");
                }
                _unitOfWorkManage.BeginTran();
                //BaseDal.DeleteAndMoveIntoHty(outboundOrder, OperateTypeEnum.自动删除);
                foreach (var item in outboundOrder.Details)
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundPickingService.cs
@@ -8,7 +8,9 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using WIDESEA_Common.CommonEnum;
using WIDESEA_Common.LocationEnum;
using WIDESEA_Common.OrderEnum;
using WIDESEA_Common.StockEnum;
@@ -16,10 +18,13 @@
using WIDESEA_Core;
using WIDESEA_Core.BaseRepository;
using WIDESEA_Core.BaseServices;
using WIDESEA_Core.Enums;
using WIDESEA_Core.Helper;
using WIDESEA_DTO.Allocate;
using WIDESEA_DTO.Basic;
using WIDESEA_DTO.Inbound;
using WIDESEA_DTO.Outbound;
using WIDESEA_IAllocateService;
using WIDESEA_IBasicService;
using WIDESEA_IInboundService;
using WIDESEA_IOutboundService;
@@ -48,8 +53,7 @@
        private readonly IESSApiService _eSSApiService;
        private readonly IInvokeMESService _invokeMESService;
        private readonly IDailySequenceService _dailySequenceService;
        private readonly IRepository<Dt_InboundOrder> _inboundOrderRepository;
        private readonly IInboundOrderDetailService _inboundOrderDetailService;
        private readonly ILogger<OutboundPickingService> _logger;
        private Dictionary<string, string> stations = new Dictionary<string, string>
@@ -69,7 +73,7 @@
        public OutboundPickingService(IRepository<Dt_PickingRecord> BaseDal, IUnitOfWorkManage unitOfWorkManage, IStockInfoService stockInfoService, IStockService stockService,
            IOutStockLockInfoService outStockLockInfoService, IStockInfoDetailService stockInfoDetailService, ILocationInfoService locationInfoService,
            IOutboundOrderDetailService outboundOrderDetailService, ISplitPackageService splitPackageService, IOutboundOrderService outboundOrderService,
            IRepository<Dt_Task> taskRepository, IESSApiService eSSApiService, ILogger<OutboundPickingService> logger, IInvokeMESService invokeMESService, IDailySequenceService dailySequenceService, IRepository<Dt_InboundOrder> inboundOrderRepository, IInboundOrderDetailService inboundOrderDetailService) : base(BaseDal)
            IRepository<Dt_Task> taskRepository, IESSApiService eSSApiService, ILogger<OutboundPickingService> logger, IInvokeMESService invokeMESService, IDailySequenceService dailySequenceService) : base(BaseDal)
        {
            _unitOfWorkManage = unitOfWorkManage;
            _stockInfoService = stockInfoService;
@@ -85,10 +89,10 @@
            _logger = logger;
            _invokeMESService = invokeMESService;
            _dailySequenceService = dailySequenceService;
            _inboundOrderRepository = inboundOrderRepository;
            _inboundOrderDetailService = inboundOrderDetailService;
        }
        #region æŸ¥è¯¢æ–¹æ³•
        // èŽ·å–æœªæ‹£é€‰åˆ—è¡¨
        public async Task<List<Dt_OutStockLockInfo>> GetUnpickedList(string orderNo, string palletCode)
        {
@@ -151,6 +155,8 @@
            return summary;
        }
        #endregion
        #region æ ¸å¿ƒä¸šåŠ¡æµç¨‹
        /// <summary>
        /// æ‹£é€‰
@@ -164,7 +170,7 @@
            try
            {
                _unitOfWorkManage.BeginTran();
                var validationResult = await ValidatePickingRequest(orderNo, palletCode, barcode);
                if (!validationResult.IsValid)
                    return WebResponseContent.Instance.Error(validationResult.ErrorMessage);
@@ -184,10 +190,10 @@
                    return WebResponseContent.Instance.Error(overPickingValidation.ErrorMessage);
                }
                //  æ‰§è¡Œåˆ†æ‹£é€»è¾‘
                // æ‰§è¡Œåˆ†æ‹£é€»è¾‘(只处理库存和锁定,不处理订单)
                var pickingResult = await ExecutePickingLogic(lockInfo, orderDetail, stockDetail, orderNo, palletCode, barcode, actualQty);
                // æ›´æ–°ç›¸å…³æ•°æ®
                // ç»Ÿä¸€æ›´æ–°è®¢å•数据(所有分支都在这里更新)
                await UpdateOrderRelatedData(orderDetail.Id, pickingResult.ActualPickedQty, orderNo);
                // è®°å½•操作历史
@@ -204,6 +210,7 @@
                return WebResponseContent.Instance.Error($"拣选确认失败:{ex.Message}");
            }
        }
        /// <summary>
        /// å–消拣选
        /// </summary>
@@ -221,19 +228,19 @@
                }
                _unitOfWorkManage.BeginTran();
                // 1. å‰ç½®éªŒè¯
                // å‰ç½®éªŒè¯
                var validationResult = await ValidateCancelRequest(orderNo, palletCode, barcode);
                if (!validationResult.IsValid)
                    return WebResponseContent.Instance.Error(validationResult.ErrorMessage);
                var (pickingRecord, lockInfo, orderDetail) = validationResult.Data;
                // 2. æ‰§è¡Œå–消逻辑
                //执行取消逻辑
                await ExecuteCancelLogic(lockInfo, pickingRecord, orderDetail, orderNo);
                _unitOfWorkManage.CommitTran();
                return WebResponseContent.Instance.OK($"取消分拣成功,恢复数量:{pickingRecord.PickQuantity}");
                return WebResponseContent.Instance.OK($"取消分拣成功");
            }
            catch (Exception ex)
            {
@@ -255,11 +262,11 @@
            {
                _unitOfWorkManage.BeginTran();
                // 1. åŸºç¡€éªŒè¯
                if (string.IsNullOrEmpty(orderNo) || string.IsNullOrEmpty(palletCode))
                    return WebResponseContent.Instance.Error("订单号和托盘码不能为空");
                // 2. èŽ·å–åº“å­˜å’Œä»»åŠ¡ä¿¡æ¯
                //  èŽ·å–åº“å­˜å’Œä»»åŠ¡ä¿¡æ¯
                var stockInfo = await _stockInfoService.Db.Queryable<Dt_StockInfo>().FirstAsync(x => x.PalletCode == palletCode);
                if (stockInfo == null)
@@ -269,23 +276,37 @@
                if (task == null)
                    return WebResponseContent.Instance.Error("未找到对应的任务信息");
                // 3. åˆ†æžéœ€è¦å›žåº“的货物
                var returnAnalysis = await AnalyzeReturnItems(orderNo, palletCode, stockInfo.Id);
                if (!returnAnalysis.HasItemsToReturn)
                    return await HandleNoReturnItems(orderNo, palletCode,task);
                //分析需要回库的货物
                //var returnAnalysis = await AnalyzeReturnItems(orderNo, palletCode, stockInfo.Id);
                //if (!returnAnalysis.HasItemsToReturn)
                //    return await HandleNoReturnItems(orderNo, palletCode, task);
                // 4. æ‰§è¡Œå›žåº“操作
                await ExecuteReturnOperations(orderNo, palletCode, stockInfo, task, returnAnalysis);
                var statusAnalysis = await AnalyzePalletStatus(orderNo, palletCode, stockInfo.Id);
                if (!statusAnalysis.HasItemsToReturn)
                    return await HandleNoReturnItems(orderNo, palletCode, task, stockInfo.Id);
                // 5. åˆ›å»ºå›žåº“任务
                await CreateReturnTaskAndHandleESS(orderNo, palletCode, task, returnAnalysis);
                // 4. æ£€æŸ¥æ˜¯å¦æœ‰è¿›è¡Œä¸­çš„任务
                if (statusAnalysis.HasActiveTasks)
                {
                    return WebResponseContent.Instance.Error($"托盘 {palletCode} æœ‰è¿›è¡Œä¸­çš„任务,不能执行回库操作");
                }
                //执行回库操作
                await ExecuteReturnOperations(orderNo, palletCode, stockInfo, task, statusAnalysis);
                await ReleaseAllLocksForReallocation(orderNo, palletCode, statusAnalysis);
                _unitOfWorkManage.CommitTran();
                // 6. æ›´æ–°è®¢å•状态(不触发MES回传)
                // åˆ›å»ºå›žåº“任务
                await CreateReturnTaskAndHandleESS(orderNo, palletCode, task, TaskTypeEnum.InPick, task.PalletType);
                // æ›´æ–°è®¢å•状态(不触发MES回传)
                await UpdateOrderStatusForReturn(orderNo);
                return WebResponseContent.Instance.OK($"回库操作成功,共回库数量:{returnAnalysis.TotalReturnQty}");
                return WebResponseContent.Instance.OK($"回库操作成功");
                //return WebResponseContent.Instance.OK($"回库操作成功,共回库数量:{statusAnalysis.TotalReturnQty}");
            }
            catch (Exception ex)
            {
@@ -295,6 +316,71 @@
            }
        }
        /// <summary>
        /// ç©ºæ‰˜ç›˜å–走接口(带订单号)
        /// éªŒè¯æ‰˜ç›˜æ˜¯å¦çœŸçš„为空,清理数据,更新订单状态,创建取托盘任务
        /// </summary>
        public async Task<WebResponseContent> RemoveEmptyPallet(string orderNo, string palletCode)
        {
            try
            {
                _unitOfWorkManage.BeginTran();
                if (string.IsNullOrEmpty(orderNo) || string.IsNullOrEmpty(palletCode))
                    return WebResponseContent.Instance.Error("订单号和托盘码不能为空");
                // æ£€æŸ¥è®¢å•是否存在
                var order = await _outboundOrderService.Db.Queryable<Dt_OutboundOrder>()
                    .Where(x => x.OrderNo == orderNo)
                    .FirstAsync();
                if (order == null)
                    return WebResponseContent.Instance.Error($"未找到订单 {orderNo}");
                //检查托盘是否存在且属于该订单
                var stockInfo = await _stockInfoService.Db.Queryable<Dt_StockInfo>()
                    .Where(x => x.PalletCode == palletCode)
                    .FirstAsync();
                if (stockInfo == null)
                    return WebResponseContent.Instance.Error($"未找到托盘 {palletCode} å¯¹åº”的库存信息");
                var statusAnalysis = await AnalyzePalletStatus(orderNo, palletCode, stockInfo.Id);
                if (!statusAnalysis.CanRemove)
                {
                    if (!statusAnalysis.IsEmptyPallet)
                    {
                        return WebResponseContent.Instance.Error($"托盘 {palletCode} ä¸Šè¿˜æœ‰è´§ç‰©ï¼Œä¸èƒ½å–èµ°");
                    }
                    if (statusAnalysis.HasActiveTasks)
                    {
                        return WebResponseContent.Instance.Error($"托盘 {palletCode} è¿˜æœ‰è¿›è¡Œä¸­çš„任务,不能取走");
                    }
                }
                // æ¸…理零库存数据
                await CleanupZeroStockData(stockInfo.Id);
                // åˆ é™¤æˆ–取消相关任务
                await HandleTaskCleanup(orderNo, palletCode);
                // æ›´æ–°è®¢å•相关数据
                await UpdateOrderData(orderNo, palletCode);
                _unitOfWorkManage.CommitTran();
                _logger.LogInformation($"空托盘取走操作成功 - è®¢å•: {orderNo}, æ‰˜ç›˜: {palletCode}, æ“ä½œäºº: {App.User.UserName}");
                return WebResponseContent.Instance.OK("空托盘取走操作成功");
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                _logger.LogError($"RemoveEmptyPallet失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error($"空托盘取走失败: {ex.Message}");
            }
        }
        #endregion
        #region åˆ†æ‹£ç¡®è®¤ç§æœ‰æ–¹æ³•
@@ -370,7 +456,7 @@
            {
                // æŸ¥æ‰¾åŒä¸€è®¢å•下的记录
                lockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                    .Where(it => it.OrderNo == orderNo &&  it.CurrentBarcode == barcode && it.Status == (int)OutLockStockStatusEnum.出库中 && it.AssignQuantity > it.PickedQty).FirstAsync();
                    .Where(it => it.OrderNo == orderNo && it.CurrentBarcode == barcode && it.Status == (int)OutLockStockStatusEnum.出库中 && it.AssignQuantity > it.PickedQty).FirstAsync();
                if (lockInfo == null)
                {
@@ -439,7 +525,7 @@
                adjustedReason = adjustedReason != null
                    ? $"{adjustedReason},防超拣限制:最终调整为{actualQty}"
                    : $"防超拣限制:从{plannedQty}调整为{actualQty}";
            }
            }
            if (adjustedReason != null)
            {
@@ -470,9 +556,11 @@
            return ValidationResult<bool>.Success(true);
        }
        private async Task<PickingResult> ExecutePickingLogic(
            Dt_OutStockLockInfo lockInfo, Dt_OutboundOrderDetail orderDetail, Dt_StockInfoDetail stockDetail,
            string orderNo, string palletCode, string barcode, decimal actualQty)
         Dt_OutStockLockInfo lockInfo, Dt_OutboundOrderDetail orderDetail, Dt_StockInfoDetail stockDetail,
         string orderNo, string palletCode, string barcode, decimal actualQty)
        {
            decimal stockQuantity = stockDetail.StockQuantity;
            var result = new PickingResult
@@ -486,50 +574,58 @@
            if (actualQty < stockQuantity)
            {
                await HandleSplitPacking(lockInfo, stockDetail, actualQty, stockQuantity, result);
                // æ‹†åŒ…场景返回实际拣选数量
                result.ActualPickedQty = actualQty;
            }
            else if (actualQty == stockQuantity)
            {
                await HandleFullPicking(lockInfo, stockDetail, actualQty, result);
                // æ•´åŒ…拣选返回实际拣选数量
                result.ActualPickedQty = actualQty;
            }
            else
            {
                await HandlePartialPicking(lockInfo, stockDetail, actualQty, stockQuantity, result);
                // éƒ¨åˆ†æ‹£é€‰è¿”回调整后的数量
                result.ActualPickedQty = result.ActualPickedQty; // å·²ç»åœ¨æ–¹æ³•内调整
            }
            return result;
        }
        private async Task HandleSplitPacking(Dt_OutStockLockInfo lockInfo, Dt_StockInfoDetail stockDetail,
            decimal actualQty, decimal stockQuantity, PickingResult result)
       decimal actualQty, decimal stockQuantity, PickingResult result)
        {
            decimal remainingStockQty = stockQuantity - actualQty;
            // 1. æ›´æ–°åŽŸæ¡ç åº“å­˜
            // æ›´æ–°åŽŸæ¡ç åº“å­˜
            stockDetail.StockQuantity = remainingStockQty;
            stockDetail.OutboundQuantity = remainingStockQty;
            await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
            // 2. ç”Ÿæˆæ–°æ¡ç 
            // ç”Ÿæˆæ–°æ¡ç 
            string newBarcode = await GenerateNewBarcode();
            // 3. åˆ›å»ºæ–°é”å®šä¿¡æ¯
            // åˆ›å»ºæ–°é”å®šä¿¡æ¯
            var newLockInfo = await CreateSplitLockInfo(lockInfo, actualQty, newBarcode);
            // 4. è®°å½•拆包历史
            // è®°å½•拆包历史
            await RecordSplitHistory(lockInfo, stockDetail, actualQty, remainingStockQty, newBarcode);
            // 5. æ›´æ–°åŽŸé”å®šä¿¡æ¯
            // æ›´æ–°åŽŸé”å®šä¿¡æ¯
            lockInfo.AssignQuantity = remainingStockQty;
            lockInfo.PickedQty = 0;
            lockInfo.Operator = App.User.UserName;
            await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync();
            // 6. è®¾ç½®ç»“æžœ
            // è®¾ç½®ç»“æžœ
            result.FinalLockInfo = newLockInfo;
            result.FinalBarcode = newBarcode;
            result.SplitResults.AddRange(CreateSplitResults(lockInfo, actualQty, remainingStockQty, newBarcode, stockDetail.Barcode));
        }
            _logger.LogInformation($"拆包分拣完成 - OrderDetailId: {lockInfo.OrderDetailId}, åˆ†æ‹£æ•°é‡: {actualQty}");
        }
        private async Task HandleFullPicking(Dt_OutStockLockInfo lockInfo, Dt_StockInfoDetail stockDetail,
            decimal actualQty, PickingResult result)
        {
@@ -581,10 +677,10 @@
            if (newOverOutQuantity > currentOrderDetail.NeedOutQuantity)
            {
                _logger.LogError($"防超拣检查失败 - OrderDetailId: {orderDetailId}, å·²å‡ºåº“: {newOverOutQuantity}, éœ€æ±‚: {currentOrderDetail.NeedOutQuantity}, æœ¬æ¬¡åˆ†æ‹£: {pickedQty}");
                decimal adjustedQty = currentOrderDetail.NeedOutQuantity - currentOrderDetail.OverOutQuantity;
                if (adjustedQty > 0)
@@ -647,7 +743,73 @@
        #endregion
        #region å–消分拣私有方法
        private async Task<ValidationResult<bool>> ValidateDataConsistencyBeforeCancel(CancelPickingContext context)
        {
            try
            {
                //  éªŒè¯è®¢å•明细数据
                var currentOrderDetail = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>()
                    .FirstAsync(x => x.Id == context.OrderDetail.Id);
                if (currentOrderDetail.OverOutQuantity < context.PickingRecord.PickQuantity)
                    return ValidationResult<bool>.Error($"订单明细已出库数量({currentOrderDetail.OverOutQuantity})小于取消数量({context.PickingRecord.PickQuantity})");
                if (currentOrderDetail.PickedQty < context.PickingRecord.PickQuantity)
                    return ValidationResult<bool>.Error($"订单明细已拣选数量({currentOrderDetail.PickedQty})小于取消数量({context.PickingRecord.PickQuantity})");
                //  éªŒè¯é”å®šä¿¡æ¯æ•°æ®
                var currentLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                    .FirstAsync(x => x.Id == context.LockInfo.Id);
                if (currentLockInfo.PickedQty < context.PickingRecord.PickQuantity)
                    return ValidationResult<bool>.Error($"锁定信息已拣选数量({currentLockInfo.PickedQty})小于取消数量({context.PickingRecord.PickQuantity})");
                ////// éªŒè¯åº“存数据
                ////var currentStockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                ////    .FirstAsync(x => x.Barcode == context.PickingRecord.Barcode && x.StockId == context.PickingRecord.StockId);
                ////if (currentStockDetail == null)
                ////    return ValidationResult<bool>.Error($"未找到对应的库存明细记录");
                ////if (currentStockDetail.Status == StockStatusEmun.入库确认.ObjToInt() ||
                ////    currentStockDetail.Status == StockStatusEmun.入库完成.ObjToInt())
                ////    return ValidationResult<bool>.Error($"条码{context.PickingRecord.Barcode}已经回库,无法取消分拣");
                // éªŒè¯çŠ¶æ€æµè½¬çš„åˆæ³•æ€§
                if (!await CanCancelPicking(currentLockInfo, null))
                    return ValidationResult<bool>.Error($"当前状态不允许取消分拣");
                return ValidationResult<bool>.Success(true);
            }
            catch (Exception ex)
            {
                _logger.LogError($"取消分拣数据一致性验证失败: {ex.Message}");
                return ValidationResult<bool>.Error($"数据验证失败: {ex.Message}");
            }
        }
        private async Task<bool> CanCancelPicking(Dt_OutStockLockInfo lockInfo, Dt_StockInfoDetail stockDetail)
        {
            // é”å®šä¿¡æ¯çŠ¶æ€æ£€æŸ¥
            if (lockInfo.Status != (int)OutLockStockStatusEnum.拣选完成)
                return false;
            ////// åº“存状态检查
            ////if (stockDetail.Status == StockStatusEmun.出库完成.ObjToInt())
            ////    return false;
            // å¦‚果是拆包记录,还需要检查父锁定信息状态
            if (lockInfo.IsSplitted == 1 && lockInfo.ParentLockId.HasValue)
            {
                var parentLock = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                    .FirstAsync(x => x.Id == lockInfo.ParentLockId.Value);
                if (parentLock == null || parentLock.Status == (int)OutLockStockStatusEnum.回库中)
                    return false;
            }
            return true;
        }
        private async Task<ValidationResult<(Dt_PickingRecord, Dt_OutStockLockInfo, Dt_OutboundOrderDetail)>> ValidateCancelRequest(string orderNo, string palletCode, string barcode)
        {
            // åŸºç¡€å‚数验证
@@ -676,7 +838,7 @@
            if (lockInfo == null)
                return ValidationResult<(Dt_PickingRecord, Dt_OutStockLockInfo, Dt_OutboundOrderDetail)>.Error("未找到对应的出库锁定信息");
            if (lockInfo.PickedQty < pickingRecord.PickQuantity)
            {
                return ValidationResult<(Dt_PickingRecord, Dt_OutStockLockInfo, Dt_OutboundOrderDetail)>.Error(
@@ -718,8 +880,8 @@
                {
                    return ValidationResult<(Dt_PickingRecord, Dt_OutStockLockInfo, Dt_OutboundOrderDetail)>.Error($"条码{barcode}已经回库,不能取消分拣");
                }
            }
            }
            return ValidationResult<(Dt_PickingRecord, Dt_OutStockLockInfo, Dt_OutboundOrderDetail)>.Success((pickingRecord, lockInfo, orderDetail));
        }
        /// <summary>
@@ -755,30 +917,25 @@
                   stockDetail.Status == StockStatusEmun.入库完成.ObjToInt();
        }
        private async Task ExecuteCancelLogic(Dt_OutStockLockInfo lockInfo, Dt_PickingRecord pickingRecord,
            Dt_OutboundOrderDetail orderDetail, string orderNo)
        Dt_OutboundOrderDetail orderDetail, string orderNo)
        {
            decimal cancelQty = pickingRecord.PickQuantity;
            var currentStockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
        .Where(it => it.Barcode == pickingRecord.Barcode && it.StockId == pickingRecord.StockId)
        .FirstAsync();
            if (currentStockDetail != null &&
                (currentStockDetail.Status == StockStatusEmun.入库确认.ObjToInt() ||
                 currentStockDetail.Status == StockStatusEmun.入库完成.ObjToInt()))
            // æ•°æ®ä¸€è‡´æ€§éªŒè¯
            var context = new CancelPickingContext
            {
                throw new Exception($"条码{pickingRecord.Barcode}已经回库,无法取消分拣");
            }
            //   æ£€æŸ¥å–消后数量不会为负数
            decimal newOverOutQuantity = orderDetail.OverOutQuantity - cancelQty;
            decimal newPickedQty = orderDetail.PickedQty - cancelQty;
                LockInfo = lockInfo,
                PickingRecord = pickingRecord,
                OrderDetail = orderDetail,
                OrderNo = orderNo,
                CancelQuantity = cancelQty
            };
            if (newOverOutQuantity < 0 || newPickedQty < 0)
            {
                throw new Exception($"取消分拣将导致数据异常:已出库{newOverOutQuantity},已拣选{newPickedQty}");
            }
            var validationResult = await ValidateDataConsistencyBeforeCancel(context);
            if (!validationResult.IsValid)
                throw new Exception(validationResult.ErrorMessage);
            //  å¤„理不同类型的取消
            // å¤„理不同类型的取消
            if (lockInfo.IsSplitted == 1 && lockInfo.ParentLockId.HasValue)
            {
                await HandleSplitBarcodeCancel(lockInfo, pickingRecord, cancelQty);
@@ -788,16 +945,23 @@
                await HandleNormalBarcodeCancel(lockInfo, pickingRecord, cancelQty);
            }
            // æ›´æ–°è®¢å•明细
            //  æ›´æ–°è®¢å•明细
            await UpdateOrderDetailOnCancel(pickingRecord.OrderDetailId, cancelQty);
            //  åˆ é™¤æ‹£é€‰è®°å½•
            await Db.Deleteable<Dt_PickingRecord>()
            var deleteResult = await Db.Deleteable<Dt_PickingRecord>()
                .Where(x => x.Id == pickingRecord.Id)
                .ExecuteCommandAsync();
            //  é‡æ–°æ£€æŸ¥è®¢å•状态
            if (deleteResult <= 0)
                throw new Exception("删除拣选记录失败");
            _logger.LogInformation($"删除拣选记录 - è®°å½•ID: {pickingRecord.Id}, æ¡ç : {pickingRecord.Barcode}");
            // é‡æ–°æ£€æŸ¥è®¢å•状态
            await UpdateOrderStatusForReturn(orderNo);
        }
        private async Task HandleSplitBarcodeCancel(Dt_OutStockLockInfo lockInfo, Dt_PickingRecord pickingRecord, decimal cancelQty)
@@ -810,59 +974,85 @@
            if (parentLockInfo == null)
                throw new Exception("未找到父锁定信息,无法取消拆包分拣");
            // æ£€æŸ¥çˆ¶æ¡ç å’Œæ‹†åŒ…条码的状态
            if (await IsLockInfoReturned(parentLockInfo))
            {
                throw new Exception($"父条码{parentLockInfo.CurrentBarcode}已经回库,无法取消拆包分拣");
            }
            if (await IsLockInfoReturned(lockInfo))
            {
                throw new Exception($"拆包条码{lockInfo.CurrentBarcode}已经回库,无法取消拆包分拣");
            }
            // æ¢å¤çˆ¶é”å®šä¿¡æ¯çš„分配数量
            parentLockInfo.AssignQuantity += cancelQty;
            parentLockInfo.Status = (int)OutLockStockStatusEnum.出库中; // æ¢å¤ä¸ºå‡ºåº“中状态
            await _outStockLockInfoService.Db.Updateable(parentLockInfo).ExecuteCommandAsync();
            // æ¢å¤åº“å­˜
            var stockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
            // æ¢å¤çˆ¶æ¡ç åº“å­˜
            var parentStockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .Where(x => x.Barcode == parentLockInfo.CurrentBarcode && x.StockId == parentLockInfo.StockId)
                .FirstAsync();
            if (stockDetail != null)
            if (parentStockDetail != null)
            {
                stockDetail.StockQuantity += cancelQty;
                stockDetail.OutboundQuantity = stockDetail.StockQuantity;
                stockDetail.Status = StockStatusEmun.出库锁定.ObjToInt();
                await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
                parentStockDetail.StockQuantity += cancelQty;
                parentStockDetail.OutboundQuantity = parentStockDetail.StockQuantity;
                parentStockDetail.Status = StockStatusEmun.出库锁定.ObjToInt();
                await _stockInfoDetailService.Db.Updateable(parentStockDetail).ExecuteCommandAsync();
                _logger.LogInformation($"恢复父条码库存 - æ¡ç : {parentStockDetail.Barcode}, æ¢å¤æ•°é‡: {cancelQty}, æ–°åº“å­˜: {parentStockDetail.StockQuantity}");
            }
            // å¤„理拆包产生的新条码库存
            var splitStockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .Where(x => x.Barcode == lockInfo.CurrentBarcode && x.StockId == lockInfo.StockId)
                .FirstAsync();
            if (splitStockDetail != null)
            {
                // åˆ é™¤æ‹†åŒ…产生的新条码库存记录
                await _stockInfoDetailService.Db.Deleteable(splitStockDetail).ExecuteCommandAsync();
                _logger.LogInformation($"删除拆包新条码库存 - æ¡ç : {splitStockDetail.Barcode}");
            }
            // æ›´æ–°æ‹†åŒ…记录状态
            await _splitPackageService.Db.Updateable<Dt_SplitPackageRecord>()
            var updateCount = await _splitPackageService.Db.Updateable<Dt_SplitPackageRecord>()
                .SetColumns(x => new Dt_SplitPackageRecord
                {
                    Status = (int)SplitPackageStatusEnum.已撤销,
                    IsReverted = true,
                    Operator = App.User.UserName,
                    RevertTime = DateTime.Now
                })
                .Where(x => x.NewBarcode == lockInfo.CurrentBarcode && !x.IsReverted)
                .ExecuteCommandAsync();
            _logger.LogInformation($"更新拆包记录状态 - æ›´æ–°è®°å½•æ•°: {updateCount}");
            // åˆ é™¤æ‹†åŒ…产生的锁定信息
            await _outStockLockInfoService.Db.Deleteable<Dt_OutStockLockInfo>()
                .Where(x => x.Id == lockInfo.Id)
                .ExecuteCommandAsync();
        }
            _logger.LogInformation($"删除拆包锁定信息 - é”å®šID: {lockInfo.Id}, æ¡ç : {lockInfo.CurrentBarcode}");
        }
        private async Task HandleNormalBarcodeCancel(Dt_OutStockLockInfo lockInfo, Dt_PickingRecord pickingRecord, decimal cancelQty)
        {
            if (await IsLockInfoReturned(lockInfo))
            {
                throw new Exception($"条码{lockInfo.CurrentBarcode}已经回库,无法取消分拣");
            }
            // æ¢å¤é”å®šä¿¡æ¯
            lockInfo.PickedQty -= cancelQty;
            if (lockInfo.PickedQty < 0) lockInfo.PickedQty = 0;
            lockInfo.Status = (int)OutLockStockStatusEnum.出库中;
            // åªæœ‰å½“拣选数量完全取消时才恢复状态
            if (lockInfo.PickedQty == 0)
            {
                lockInfo.Status = (int)OutLockStockStatusEnum.出库中;
            }
            lockInfo.Operator = App.User.UserName;
            await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync();
            _logger.LogInformation($"恢复锁定信息 - é”å®šID: {lockInfo.Id}, æ‰£å‡æ‹£é€‰æ•°é‡: {cancelQty}, æ–°å·²æ‹£é€‰æ•°é‡: {lockInfo.PickedQty}");
            // æ¢å¤åº“å­˜
            var stockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
@@ -873,36 +1063,57 @@
            {
                stockDetail.StockQuantity += cancelQty;
                stockDetail.OutboundQuantity = stockDetail.StockQuantity;
                stockDetail.Status = StockStatusEmun.出库锁定.ObjToInt();
                // æ¢å¤åº“存状态
                if (stockDetail.Status == StockStatusEmun.出库完成.ObjToInt())
                {
                    stockDetail.Status = StockStatusEmun.出库锁定.ObjToInt();
                }
                await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
                _logger.LogInformation($"恢复库存 - æ¡ç : {stockDetail.Barcode}, æ¢å¤æ•°é‡: {cancelQty}, " +
                                      $"新库存: {stockDetail.StockQuantity}, æ–°çŠ¶æ€: {stockDetail.Status}");
            }
            else
            {
                _logger.LogWarning($"未找到库存记录 - æ¡ç : {pickingRecord.Barcode}, åº“å­˜ID: {pickingRecord.StockId}");
            }
        }
        private async Task UpdateOrderDetailOnCancel(int orderDetailId, decimal cancelQty)
        {
            // èŽ·å–æœ€æ–°çš„è®¢å•æ˜Žç»†æ•°æ®
            // èŽ·å–æœ€æ–°çš„è®¢å•æ˜Žç»†æ•°æ®ï¼ˆå¸¦é”ï¼‰
            var currentOrderDetail = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>()
                .With(SqlWith.RowLock)
                .FirstAsync(x => x.Id == orderDetailId);
            decimal newOverOutQuantity = currentOrderDetail.OverOutQuantity - cancelQty;
            decimal newPickedQty = currentOrderDetail.PickedQty - cancelQty;
            // æ£€æŸ¥å–消后数量不会为负数
            if (newOverOutQuantity < 0 || newPickedQty < 0)
            {
                throw new Exception($"取消分拣将导致已出库数量({newOverOutQuantity})或已拣选数量({newPickedQty})为负数");
            }
            // ä¸¥æ ¼æ£€æŸ¥å–消后数量不会为负数
            if (newOverOutQuantity < 0)
                throw new Exception($"取消分拣将导致已出库数量({newOverOutQuantity})为负数");
            await _outboundOrderDetailService.Db.Updateable<Dt_OutboundOrderDetail>()
            if (newPickedQty < 0)
                throw new Exception($"取消分拣将导致已拣选数量({newPickedQty})为负数");
            // æ›´æ–°è®¢å•明细
            var updateResult = await _outboundOrderDetailService.Db.Updateable<Dt_OutboundOrderDetail>()
                .SetColumns(it => new Dt_OutboundOrderDetail
                {
                    PickedQty = newPickedQty,
                    OverOutQuantity = newOverOutQuantity,
                    OverOutQuantity = newOverOutQuantity
                })
                .Where(it => it.Id == orderDetailId)
                .ExecuteCommandAsync();
        }
            if (updateResult <= 0)
                throw new Exception("更新订单明细失败");
            _logger.LogInformation($"更新订单明细 - OrderDetailId: {orderDetailId}, " +
                                  $"扣减已出库: {cancelQty}, æ–°å·²å‡ºåº“: {newOverOutQuantity}, " +
                                  $"扣减已拣选: {cancelQty}, æ–°å·²æ‹£é€‰: {newPickedQty}");
        }
        #endregion
        #region å›žåº“操作私有方法
@@ -946,58 +1157,6 @@
            return task;
        }
        private async Task<ReturnAnalysisResult> AnalyzeReturnItems(string orderNo, string palletCode, int stockId)
        {
            var result = new ReturnAnalysisResult();
            // æƒ…况1:获取未分拣的出库锁定记录
            var remainingLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(it => it.OrderNo == orderNo &&
                           it.PalletCode == palletCode &&
                           it.Status == (int)OutLockStockStatusEnum.出库中)
                .ToListAsync();
            if (remainingLocks.Any())
            {
                result.HasRemainingLocks = true;
                result.RemainingLocks = remainingLocks;
                result.RemainingLocksReturnQty = remainingLocks.Sum(x => x.AssignQuantity - x.PickedQty);
            }
            // æƒ…况2:检查托盘上是否有其他库存货物
            var palletStockGoods = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .Where(it => it.StockId == stockId &&
                            (it.Status == StockStatusEmun.入库确认.ObjToInt() ||
                             it.Status == StockStatusEmun.入库完成.ObjToInt() ||
                             it.Status == StockStatusEmun.出库锁定.ObjToInt()))
                .Where(it => it.StockQuantity > 0)
                .ToListAsync();
            if (palletStockGoods.Any())
            {
                result.HasPalletStockGoods = true;
                result.PalletStockGoods = palletStockGoods;
                result.PalletStockReturnQty = palletStockGoods.Sum(x => x.StockQuantity);
            }
            // æƒ…况3:检查拆包记录
            var splitRecords = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
                .Where(it => it.OrderNo == orderNo && it.PalletCode == palletCode && !it.IsReverted && it.Status != (int)SplitPackageStatusEnum.已回库)
                .ToListAsync();
            if (splitRecords.Any())
            {
                result.HasSplitRecords = true;
                result.SplitRecords = splitRecords;
                result.SplitReturnQty = await CalculateSplitReturnQuantity(splitRecords, stockId);
            }
            result.TotalReturnQty = result.RemainingLocksReturnQty + result.PalletStockReturnQty + result.SplitReturnQty;
            result.HasItemsToReturn = result.TotalReturnQty > 0;
            return result;
        }
        private async Task<decimal> CalculateSplitReturnQuantity(List<Dt_SplitPackageRecord> splitRecords, int stockId)
        {
            decimal totalQty = 0;
@@ -1005,11 +1164,14 @@
            foreach (var splitRecord in splitRecords)
            {
                if (splitRecord.Status != (int)SplitPackageStatusEnum.已撤销)
                    continue;
                // æ£€æŸ¥åŽŸæ¡ç 
                if (!processedBarcodes.Contains(splitRecord.OriginalBarcode))
                {
                    var originalStock = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                        .Where(it => it.Barcode == splitRecord.OriginalBarcode && it.StockId == stockId)
                        .Where(it => it.Barcode == splitRecord.OriginalBarcode && it.StockId == stockId &&
                           it.Status != StockStatusEmun.出库完成.ObjToInt())
                        .FirstAsync();
                    if (originalStock != null && originalStock.StockQuantity > 0)
@@ -1023,7 +1185,7 @@
                if (!processedBarcodes.Contains(splitRecord.NewBarcode))
                {
                    var newStock = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                        .Where(it => it.Barcode == splitRecord.NewBarcode && it.StockId == stockId)
                        .Where(it => it.Barcode == splitRecord.NewBarcode && it.StockId == stockId && it.Status != StockStatusEmun.出库完成.ObjToInt())
                        .FirstAsync();
                    if (newStock != null && newStock.StockQuantity > 0)
@@ -1037,57 +1199,225 @@
            return totalQty;
        }
        private async Task<WebResponseContent> HandleNoReturnItems(string orderNo, string palletCode,Dt_Task originalTask)
        private async Task<WebResponseContent> HandleNoReturnItems(string orderNo, string palletCode, Dt_Task originalTask, int stockInfoId)
        {
            // æ£€æŸ¥æ˜¯å¦æ‰€æœ‰è´§ç‰©éƒ½å·²æ‹£é€‰å®Œæˆ
            var allPicked = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(it => it.OrderNo == orderNo && it.PalletCode == palletCode)
                .AnyAsync(it => it.Status == (int)OutLockStockStatusEnum.拣选完成);
            //var allPicked = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
            //    .Where(it => it.OrderNo == orderNo && it.PalletCode == palletCode)
            //    .AnyAsync(it => it.Status == (int)OutLockStockStatusEnum.拣选完成);
            if (allPicked)
            //if (allPicked)
            //{
            //    // åˆ é™¤åŽŸå§‹å‡ºåº“ä»»åŠ¡ ç»„空盘   ç©ºç›˜å›žåº“
            //    //await _taskRepository.Db.Deleteable(originalTask).ExecuteCommandAsync();
            //    return WebResponseContent.Instance.OK("所有货物已拣选完成,托盘为空");
            //}
            //else
            //{
            //    // åˆ é™¤åŽŸå§‹å‡ºåº“ä»»åŠ¡
            //    //await _taskRepository.Db.Deleteable(originalTask).ExecuteCommandAsync();
            //    return WebResponseContent.Instance.Error("没有需要回库的剩余货物");
            //}
            try
            {
                // åˆ é™¤åŽŸå§‹å‡ºåº“ä»»åŠ¡
                await _taskRepository.Db.Deleteable(originalTask).ExecuteCommandAsync();
                return WebResponseContent.Instance.OK("所有货物已拣选完成,托盘为空");
                var locationtype = 0;
                var stockInfo = await _stockInfoService.Db.Queryable<Dt_StockInfo>()
                        .Where(x => x.PalletCode == palletCode)
                        .FirstAsync();
                if (stockInfo == null)
                {
                    var firstLocation = await _locationInfoService.Db.Queryable<Dt_LocationInfo>().FirstAsync(x => x.LocationCode == originalTask.SourceAddress);
                    locationtype = firstLocation?.LocationType ?? 1;
                }
                else
                {
                    locationtype = stockInfo.LocationType;
                    _stockInfoService.DeleteData(stockInfo);
                }
                var targetAddress = originalTask.TargetAddress;
                await CleanupZeroStockData(stockInfoId);
                var emptystockInfo = new Dt_StockInfo() { PalletType = PalletTypeEnum.Empty.ObjToInt(), StockStatus = StockStatusEmun.组盘暂存.ObjToInt(), PalletCode = palletCode, LocationType = locationtype };
                emptystockInfo.Details = new List<Dt_StockInfoDetail>();
                _stockInfoService.AddMaterielGroup(emptystockInfo);
                //空托盘如何处理  è¿˜æœ‰ä¸€ä¸ªå‡ºåº“任务要处理。
                originalTask.PalletType = PalletTypeEnum.Empty.ObjToInt();
                await CreateReturnTaskAndHandleESS(orderNo, palletCode, originalTask, TaskTypeEnum.InEmpty, PalletTypeEnum.Empty.ObjToInt());
            }
            else
            catch (Exception ex)
            {
                // åˆ é™¤åŽŸå§‹å‡ºåº“ä»»åŠ¡
                await _taskRepository.Db.Deleteable(originalTask).ExecuteCommandAsync();
                return WebResponseContent.Instance.Error("没有需要回库的剩余货物");
                _logger.LogError($" HandleNoReturnItems  å¤±è´¥: {ex.Message}");
                return WebResponseContent.Instance.Error($" å›žåº“空托盘失败!");
            }
            //空托盘如何处理  è¿˜æœ‰ä¸€ä¸ªå‡ºåº“任务要处理。
            return WebResponseContent.Instance.OK("空托盘回库任务创建成功");
        }
        private async Task ExecuteReturnOperations(string orderNo, string palletCode, Dt_StockInfo stockInfo,
            Dt_Task task, ReturnAnalysisResult analysis)
            Dt_Task task, PalletStatusAnalysis analysis)
        {
            // æƒ…况1:处理未分拣的出库锁定记录
            if (analysis.HasRemainingLocks)
            {
                await HandleRemainingLocksReturn(analysis.RemainingLocks, stockInfo.Id);
                // å…³é”®ï¼šæ›´æ–°è®¢å•明细的已拣选数量
                await UpdateOrderDetailsOnReturn(analysis.RemainingLocks);
                // await UpdateOrderDetailsOnReturn(analysis.RemainingLocks);
            }
            // å¤„理托盘上其他库存货物
            if (analysis.HasPalletStockGoods)
            {
                await HandlePalletStockGoodsReturn(analysis.PalletStockGoods);
                var validStockGoods = analysis.PalletStockGoods
            .Where(x => x.Status != StockStatusEmun.出库完成.ObjToInt())
            .ToList();
                if (validStockGoods.Any())
                {
                    await HandlePalletStockGoodsReturn(analysis.PalletStockGoods);
                }
                else
                {
                    _logger.LogInformation("没有有效的库存货物需要回库");
                }
            }
            // å¤„理拆包记录
            if (analysis.HasSplitRecords)
            {
                await HandleSplitRecordsReturn(analysis.SplitRecords, orderNo, palletCode);
                var validSplitRecords = analysis.SplitRecords
            .Where(x => x.Status != (int)SplitPackageStatusEnum.已拣选)
            .ToList();
                if (validSplitRecords.Any())
                {
                    await HandleSplitRecordsReturn(analysis.SplitRecords, orderNo, palletCode);
                }
                else
                {
                    _logger.LogInformation("没有有效的拆包记录需要处理");
                }
            }
            // æ›´æ–°åº“存主表状态
            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();
@@ -1105,7 +1435,13 @@
            foreach (var lockInfo in remainingLocks)
            {
                decimal returnQty = lockInfo.AssignQuantity - lockInfo.PickedQty;
                if (returnQty <= 0)
                {
                    _logger.LogWarning($"锁定记录{lockInfo.Id}无需回库,分配数量: {lockInfo.AssignQuantity}, å·²æ‹£é€‰: {lockInfo.PickedQty}");
                    continue;
                }
                _logger.LogInformation($"处理锁定记录回库 - é”å®šID: {lockInfo.Id}, æ¡ç : {lockInfo.CurrentBarcode}, å›žåº“数量: {returnQty}");
                // æŸ¥æ‰¾å¯¹åº”的库存明细
                var stockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                    .Where(it => it.Barcode == lockInfo.CurrentBarcode && it.StockId == lockInfo.StockId)
@@ -1121,23 +1457,23 @@
                else
                {
                    // åˆ›å»ºæ–°çš„库存记录
                    var newStockDetail = new Dt_StockInfoDetail
                    {
                        StockId = lockInfo.StockId,
                        MaterielCode = lockInfo.MaterielCode,
                        MaterielName = lockInfo.MaterielName,
                        OrderNo = lockInfo.OrderNo,
                        BatchNo = lockInfo.BatchNo,
                        StockQuantity = returnQty,
                        OutboundQuantity = 0,
                        Barcode = lockInfo.CurrentBarcode,
                        InboundOrderRowNo = "",
                        Status = StockStatusEmun.入库确认.ObjToInt(),
                        SupplyCode = lockInfo.SupplyCode,
                        WarehouseCode = lockInfo.WarehouseCode,
                        Unit = lockInfo.Unit,
                    };
                    await _stockInfoDetailService.Db.Insertable(newStockDetail).ExecuteCommandAsync();
                    //var newStockDetail = new Dt_StockInfoDetail
                    //{
                    //    StockId = lockInfo.StockId,
                    //    MaterielCode = lockInfo.MaterielCode,
                    //    MaterielName = lockInfo.MaterielName,
                    //    OrderNo = lockInfo.OrderNo,
                    //    BatchNo = lockInfo.BatchNo,
                    //    StockQuantity = returnQty,
                    //    OutboundQuantity = 0,
                    //    Barcode = lockInfo.CurrentBarcode,
                    //    InboundOrderRowNo = "",
                    //    Status = StockStatusEmun.入库确认.ObjToInt(),
                    //    SupplyCode = lockInfo.SupplyCode,
                    //    WarehouseCode = lockInfo.WarehouseCode,
                    //    Unit = lockInfo.Unit,
                    //};
                    //await _stockInfoDetailService.Db.Insertable(newStockDetail).ExecuteCommandAsync();
                }
            }
        }
@@ -1180,24 +1516,42 @@
            foreach (var stockGood in palletStockGoods)
            {
                _logger.LogInformation($"待回库货物 - æ¡ç : {stockGood.Barcode}, æ•°é‡: {stockGood.StockQuantity}, å½“前状态: {stockGood.Status}");
            // æ¢å¤åº“存状态
            stockGood.OutboundQuantity = 0;
                stockGood.Status = StockStatusEmun.入库确认.ObjToInt();
                await _stockInfoDetailService.Db.Updateable(stockGood).ExecuteCommandAsync();
                if (stockGood.Status != StockStatusEmun.出库完成.ObjToInt())
                {
                    stockGood.OutboundQuantity = 0;
                    stockGood.Status = StockStatusEmun.入库确认.ObjToInt();
                    await _stockInfoDetailService.Db.Updateable(stockGood).ExecuteCommandAsync();
                    _logger.LogInformation($"库存货物回库完成 - æ¡ç : {stockGood.Barcode}, æ–°çŠ¶æ€: {stockGood.Status}");
                }
                else
                {
                    _logger.LogWarning($"跳过已出库完成的货物 - æ¡ç : {stockGood.Barcode}");
                }
            }
        }
        private async Task HandleSplitRecordsReturn(List<Dt_SplitPackageRecord> splitRecords, string orderNo, string palletCode)
        {
            var validRecords = splitRecords.Where(x => x.Status != (int)SplitPackageStatusEnum.已拣选).ToList();
            if (!validRecords.Any())
            {
                _logger.LogInformation("没有需要回库的拆包记录");
                return;
            }
            _logger.LogInformation($"更新{validRecords.Count}条拆包记录状态为已回库");
            // æ›´æ–°æ‹†åŒ…记录状态
            await _splitPackageService.Db.Updateable<Dt_SplitPackageRecord>()
                .SetColumns(x => new Dt_SplitPackageRecord
                {
                    Status = (int)SplitPackageStatusEnum.已回库,
                    Operator = App.User.UserName
                })
                .Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode && !x.IsReverted)
                .Where(x => validRecords.Select(r => r.Id).Contains(x.Id))
                .ExecuteCommandAsync();
        }
@@ -1216,7 +1570,7 @@
        /// <param name="originalTask"></param>
        /// <param name="analysis"></param>
        /// <returns></returns>
        private async Task CreateReturnTaskAndHandleESS(string orderNo, string palletCode, Dt_Task originalTask, ReturnAnalysisResult analysis)
        private async Task CreateReturnTaskAndHandleESS(string orderNo, string palletCode, Dt_Task originalTask, TaskTypeEnum taskTypeEnum, int palletType)
        {
            var firstLocation = await _locationInfoService.Db.Queryable<Dt_LocationInfo>()
                .FirstAsync(x => x.LocationCode == originalTask.SourceAddress);
@@ -1230,22 +1584,26 @@
                Grade = 0,
                PalletCode = palletCode,
                NextAddress = "",
                OrderNo = originalTask.OrderNo,
                // OrderNo = originalTask.OrderNo,
                OrderNo = orderNo,
                Roadway = newLocation.RoadwayNo,
                SourceAddress = stations[originalTask.TargetAddress],
                TargetAddress = newLocation.LocationCode,
                TaskStatus = TaskStatusEnum.New.ObjToInt(),
                TaskType = TaskTypeEnum.InPick.ObjToInt(),
                PalletType = originalTask.PalletType,
                TaskType = taskTypeEnum.ObjToInt(),
                PalletType = palletType,
                WarehouseId = originalTask.WarehouseId
            };
            // ä¿å­˜å›žåº“任务
            await _taskRepository.Db.Insertable(returnTask).ExecuteCommandAsync();
            var targetAddress = originalTask.TargetAddress;
            var targetAddress = originalTask.TargetAddress;
            // åˆ é™¤åŽŸå§‹å‡ºåº“ä»»åŠ¡
            await _taskRepository.Db.Deleteable(originalTask).ExecuteCommandAsync();
            _taskRepository.DeleteAndMoveIntoHty(originalTask, OperateTypeEnum.自动完成);
            // await _taskRepository.Db.Deleteable(originalTask).ExecuteCommandAsync();
            // ç»™ ESS å‘送流动信号和创建任务
            await SendESSCommands(palletCode, targetAddress, returnTask);
@@ -1269,18 +1627,15 @@
                    containerCode = palletCode
                });
                if (moveResult)
                //if (moveResult)
                //{
                // 2. åˆ›å»ºå›žåº“任务
                var essTask = new TaskModel()
                {
                    // 2. åˆ›å»ºå›žåº“任务
                    var essTask = new TaskModel()
                    {
                        taskType = "putaway",
                        taskGroupCode = "",
                        groupPriority = 0,
                        tasks = new List<TasksType>
                    {
                        new()
                        {
                    taskType = "putaway",
                    taskGroupCode = "",
                    groupPriority = 0,
                    tasks = new List<TasksType>{  new() {
                            taskCode = returnTask.TaskNum.ToString(),
                            taskPriority = 0,
                            taskDescribe = new TaskDescribeType
@@ -1293,13 +1648,12 @@
                                deadline = 0,
                                storageTag = ""
                            }
                        }
                    }
                    };
                        } }
                };
                    var resultTask = await _eSSApiService.CreateTaskAsync(essTask);
                    _logger.LogInformation($"ReturnRemaining åˆ›å»ºä»»åŠ¡æˆåŠŸ: {resultTask}");
                }
                var resultTask = await _eSSApiService.CreateTaskAsync(essTask);
                _logger.LogInformation($"ReturnRemaining åˆ›å»ºä»»åŠ¡æˆåŠŸ: {resultTask}");
                //}
            }
            catch (Exception ex)
            {
@@ -1342,15 +1696,19 @@
                if (outboundOrder.OrderStatus != newStatus)
                {
                    await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
                        .SetColumns(x => x.OrderStatus == newStatus)
                        .SetColumns(x => new Dt_OutboundOrder
                        {
                            OrderStatus = newStatus,
                            Operator = App.User.UserName,
                        })
                        .Where(x => x.OrderNo == orderNo)
                        .ExecuteCommandAsync();
                    // åªæœ‰æ­£å¸¸åˆ†æ‹£å®Œæˆæ—¶æ‰å‘MES反馈
                    if (allCompleted && newStatus == (int)OutOrderStatusEnum.出库完成)
                    {
                        await HandleOrderCompletion(outboundOrder, orderNo);
                    }
                    //if (allCompleted && newStatus == (int)OutOrderStatusEnum.出库完成)
                    //{
                    //    await HandleOrderCompletion(outboundOrder, orderNo);
                    //}
                }
            }
            catch (Exception ex)
@@ -1358,6 +1716,8 @@
                _logger.LogError($"CheckAndUpdateOrderStatus失败 - OrderNo: {orderNo}, Error: {ex.Message}");
            }
        }
        private async Task UpdateOrderStatusForReturn(string orderNo)
        {
@@ -1389,7 +1749,11 @@
                if (outboundOrder.OrderStatus != newStatus)
                {
                    await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
                        .SetColumns(x => x.OrderStatus == newStatus)
                          .SetColumns(x => new Dt_OutboundOrder
                          {
                              OrderStatus = newStatus,
                              Operator = App.User.UserName,
                          })
                        .Where(x => x.OrderNo == orderNo)
                        .ExecuteCommandAsync();
@@ -1405,76 +1769,480 @@
        private async Task HandleOrderCompletion(Dt_OutboundOrder outboundOrder, string orderNo)
        {
            // è°ƒæ‹¨å‡ºåº“和重检出库不需要反馈MES
            if (outboundOrder.OrderType == OutOrderTypeEnum.Allocate.ObjToInt() ||
                outboundOrder.OrderType == OutOrderTypeEnum.ReCheck.ObjToInt())
            if (outboundOrder.OrderType == OutOrderTypeEnum.Allocate.ObjToInt())
            {
                return;
            }
            try
            {
                var feedmodel = new FeedbackOutboundRequestModel
                var allocate = _allocateService.Repository.QueryData(x => x.UpperOrderNo == outboundOrder.UpperOrderNo).First();
                var allocatefeedmodel = new AllocateDto
                {
                    reqCode = Guid.NewGuid().ToString(),
                    reqTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
                    business_type = outboundOrder.BusinessType,
                    factoryArea = outboundOrder.FactoryArea,
                    operationType = 1,
                    ReqCode = Guid.NewGuid().ToString(),
                    ReqTime = DateTime.Now.ToString(),
                    BusinessType = "3",
                    FactoryArea = outboundOrder.FactoryArea,
                    OperationType = 1,
                    Operator = App.User.UserName,
                    orderNo = outboundOrder.UpperOrderNo,
                    status = outboundOrder.OrderStatus,
                    details = new List<FeedbackOutboundDetailsModel>()
                };
                    OrderNo = outboundOrder.UpperOrderNo,
                   // documentsNO = outboundOrder.OrderNo,
                   // status = outboundOrder.OrderStatus,
                    fromWarehouse = allocate?.FromWarehouse ?? "",
                    toWarehouse = allocate?.ToWarehouse ?? "",
                    Details = new List<AllocateDtoDetail>()
                };
                // åªèŽ·å–å·²æ‹£é€‰å®Œæˆçš„é”å®šè®°å½•
                var lists = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                    .Where(x => x.OrderNo == orderNo && x.Status == (int)OutLockStockStatusEnum.拣选完成)
                    .ToListAsync();
                var groupedData = lists.GroupBy(item => new { item.MaterielCode, item.lineNo, item.Unit, item.WarehouseCode })
                   .Select(group => new FeedbackOutboundDetailsModel
                   .Select(group => new AllocateDtoDetail
                   {
                       materialCode = group.Key.MaterielCode,
                       lineNo = group.Key.lineNo,
                       warehouseCode = group.Key.WarehouseCode,
                       qty = group.Sum(x => x.PickedQty),
                       currentDeliveryQty = group.Sum(x => x.PickedQty),
                       unit = group.Key.Unit,
                       barcodes = group.Select(row => new WIDESEA_DTO.Outbound.BarcodesModel
                       MaterialCode = group.Key.MaterielCode,
                       LineNo = group.Key.lineNo,
                       WarehouseCode = group.Key.WarehouseCode,
                       Qty = group.Sum(x => x.PickedQty),
                       Unit = group.Key.Unit,
                       Barcodes = group.Select(row => new BarcodeInfo
                       {
                           barcode = row.CurrentBarcode,
                           supplyCode = row.SupplyCode,
                           batchNo = row.BatchNo,
                           unit = row.Unit,
                           qty = row.PickedQty
                           Barcode = row.CurrentBarcode,
                           SupplyCode = row.SupplyCode,
                           BatchNo = row.BatchNo,
                           Unit = row.Unit,
                           Qty = row.PickedQty
                       }).ToList()
                   }).ToList();
                allocatefeedmodel.Details = groupedData;
                feedmodel.details = groupedData;
                var result = await _invokeMESService.FeedbackOutbound(feedmodel);
                var result = await _invokeMESService.FeedbackAllocate(allocatefeedmodel);
                if (result != null && result.code == 200)
                {
                {
                    await _outboundOrderDetailService.Db.Updateable<Dt_OutboundOrderDetail>()
                        .SetColumns(x => x.ReturnToMESStatus == 1)
                        .Where(x => x.OrderId == outboundOrder.Id)
                        .ExecuteCommandAsync();
                           .SetColumns(x => x.ReturnToMESStatus == 1)
                           .Where(x => x.OrderId == outboundOrder.Id).ExecuteCommandAsync();
                    await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
                        .SetColumns(x => x.ReturnToMESStatus == 1)
                        .Where(x => x.OrderNo == orderNo)
                        .ExecuteCommandAsync();
                          .SetColumns(x => new Dt_OutboundOrder
                          {
                              ReturnToMESStatus = 1,
                              Operator = App.User.UserName,
                          }).Where(x => x.OrderNo == orderNo).ExecuteCommandAsync();
                }
            }
            catch (Exception ex)
            else if (outboundOrder.OrderType == OutOrderTypeEnum.ReCheck.ObjToInt())
            {
                _logger.LogError($"FeedbackOutbound失败 - OrderNo: {orderNo}, Error: {ex.Message}");
            }
            else
            {
                try
                {
                    var feedmodel = new FeedbackOutboundRequestModel
                    {
                        reqCode = Guid.NewGuid().ToString(),
                        reqTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
                        business_type = outboundOrder.BusinessType,
                        factoryArea = outboundOrder.FactoryArea,
                        operationType = 1,
                        Operator = App.User.UserName,
                        orderNo = outboundOrder.UpperOrderNo,
                        documentsNO = outboundOrder.OrderNo,
                        status = outboundOrder.OrderStatus,
                        details = new List<FeedbackOutboundDetailsModel>()
                    };
                    // åªèŽ·å–å·²æ‹£é€‰å®Œæˆçš„é”å®šè®°å½•
                    var lists = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                        .Where(x => x.OrderNo == orderNo && x.Status == (int)OutLockStockStatusEnum.拣选完成)
                        .ToListAsync();
                    var groupedData = lists.GroupBy(item => new { item.MaterielCode, item.lineNo, item.Unit, item.WarehouseCode })
                       .Select(group => new FeedbackOutboundDetailsModel
                       {
                           materialCode = group.Key.MaterielCode,
                           lineNo = group.Key.lineNo,
                           warehouseCode = group.Key.WarehouseCode,
                           qty = group.Sum(x => x.PickedQty),
                           currentDeliveryQty = group.Sum(x => x.PickedQty),
                           unit = group.Key.Unit,
                           barcodes = group.Select(row => new WIDESEA_DTO.Outbound.BarcodesModel
                           {
                               barcode = row.CurrentBarcode,
                               supplyCode = row.SupplyCode,
                               batchNo = row.BatchNo,
                               unit = row.Unit,
                               qty = row.PickedQty
                           }).ToList()
                       }).ToList();
                    feedmodel.details = groupedData;
                    var result = await _invokeMESService.FeedbackOutbound(feedmodel);
                    if (result != null && result.code == 200)
                    {
                        await _outboundOrderDetailService.Db.Updateable<Dt_OutboundOrderDetail>()
                            .SetColumns(x => x.ReturnToMESStatus == 1)
                            .Where(x => x.OrderId == outboundOrder.Id)
                            .ExecuteCommandAsync();
                        await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
                              .SetColumns(x => new Dt_OutboundOrder
                              {
                                  ReturnToMESStatus = 1,
                                  Operator = App.User.UserName,
                              })
                            .Where(x => x.OrderNo == orderNo)
                            .ExecuteCommandAsync();
                    }
                    _logger.LogError($"FeedbackOutbound成功 - OrderNo: {orderNo}, {JsonSerializer.Serialize(result)}");
                }
                catch (Exception ex)
                {
                    _logger.LogError($"FeedbackOutbound失败 - OrderNo: {orderNo}, Error: {ex.Message}");
                }
            }
        }
        #endregion
        #region ç©ºæ‰˜ç›˜
        /// <summary>
        /// æ¸…理零库存数据
        /// </summary>
        private async Task CleanupZeroStockData(int stockId)
        {
            try
            {
                // 1. åˆ é™¤åº“存数量为0的明细记录
                var deleteDetailCount = await _stockInfoDetailService.Db.Deleteable<Dt_StockInfoDetail>()
                    .Where(x => x.StockId == stockId && x.StockQuantity == 0 && (x.Status == StockStatusEmun.出库完成.ObjToInt() || x.Status ==
                                          StockStatusEmun.入库完成.ObjToInt()))
                    .ExecuteCommandAsync();
                await _stockInfoService.Db.Deleteable<Dt_StockInfo>()
                   .Where(x => x.Id == stockId).ExecuteCommandAsync();
                _logger.LogInformation($"清理零库存明细记录 - StockId: {stockId}, åˆ é™¤è®°å½•æ•°: {deleteDetailCount}");
            }
            catch (Exception ex)
            {
                _logger.LogWarning($"清理零库存数据失败 - StockId: {stockId}, Error: {ex.Message}");
                // æ³¨æ„ï¼šæ¸…理失败不应该影响主流程
            }
        }
        /// <summary>
        /// å¤„理任务清理(按订单和托盘)
        /// </summary>
        private async Task HandleTaskCleanup(string orderNo, string palletCode)
        {
            try
            {
                // 1. æŸ¥æ‰¾æ‰€æœ‰ä¸Žè¯¥è®¢å•和托盘相关的任务
                var tasks = await _taskRepository.Db.Queryable<Dt_Task>().Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode).ToListAsync();
                if (tasks.Any())
                {
                    foreach (var task in tasks)
                    {
                        task.TaskStatus = (int)TaskStatusEnum.Finish;
                    }
                    // await _taskRepository.Db.Updateable(tasks).ExecuteCommandAsync();
                    _taskRepository.DeleteAndMoveIntoHty(tasks, OperateTypeEnum.自动完成);
                    _logger.LogInformation($"完成{tasks.Count}个托盘任务 - è®¢å•: {orderNo}, æ‰˜ç›˜: {palletCode}");
                }
            }
            catch (Exception ex)
            {
                _logger.LogWarning($"处理任务清理失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Error: {ex.Message}");
                throw new Exception($"任务清理失败: {ex.Message}");
            }
        }
        /// <summary>
        /// æ›´æ–°è®¢å•相关数据
        /// </summary>
        private async Task UpdateOrderData(string orderNo, string palletCode)
        {
            try
            {
                // æ£€æŸ¥è®¢å•是否还有其他托盘在处理中
                var otherActivePallets = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                    .Where(x => x.OrderNo == orderNo &&
                               x.PalletCode != palletCode &&
                             (x.Status == (int)OutLockStockStatusEnum.出库中 || x.Status == (int)OutLockStockStatusEnum.回库中))
                    .AnyAsync();
                var otherActiveTasks = await _taskRepository.Db.Queryable<Dt_Task>()
                    .Where(x => x.OrderNo == orderNo &&
                               x.PalletCode != palletCode
                    // && x.TaskStatus.In((int)TaskStatusEnum.待执行, (int)TaskStatusEnum.执行中)
                     )
                    .AnyAsync();
                // å¦‚果没有其他托盘在处理,检查订单是否应该完成
                if (!otherActivePallets && !otherActiveTasks)
                {
                    await CheckAndUpdateOrderCompletion(orderNo);
                }
                else
                {
                    _logger.LogInformation($"订单 {orderNo} è¿˜æœ‰å…¶ä»–托盘在处理,不更新订单状态");
                }
                // 3. æ›´æ–°æ‹£é€‰è®°å½•状态(可选)
                await UpdatePickingRecordsStatus(orderNo, palletCode);
            }
            catch (Exception ex)
            {
                _logger.LogWarning($"更新订单数据失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Error: {ex.Message}");
                throw new Exception($"更新订单数据失败: {ex.Message}");
            }
        }
        /// <summary>
        /// æ£€æŸ¥å¹¶æ›´æ–°è®¢å•完成状态
        /// </summary>
        private async Task CheckAndUpdateOrderCompletion(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)
                .ToListAsync();
            bool allCompleted = true;
            foreach (var detail in orderDetails)
            {
                if (detail.OverOutQuantity < detail.NeedOutQuantity)
                {
                    allCompleted = false;
                    break;
                }
            }
            var outboundOrder = await _outboundOrderService.Db.Queryable<Dt_OutboundOrder>()
                .FirstAsync(x => x.OrderNo == orderNo);
            if (outboundOrder != null && allCompleted && outboundOrder.OrderStatus != (int)OutOrderStatusEnum.出库完成)
            {
                outboundOrder.OrderStatus = (int)OutOrderStatusEnum.出库完成;
                outboundOrder.Operator = App.User.UserName;
                await _outboundOrderService.Db.Updateable(outboundOrder).ExecuteCommandAsync();
                _logger.LogInformation($"订单 {orderNo} å·²æ ‡è®°ä¸ºå‡ºåº“完成");
                // å‘MES反馈订单完成(如果需要)
                await HandleOrderCompletion(outboundOrder, orderNo);
            }
        }
        /// <summary>
        /// æ›´æ–°æ‹£é€‰è®°å½•状态
        /// </summary>
        private async Task UpdatePickingRecordsStatus(string orderNo, string palletCode)
        {
            try
            {
                // å¯ä»¥å°†ç›¸å…³çš„æ‹£é€‰è®°å½•标记为已完成
                var pickingRecords = await Db.Queryable<Dt_PickingRecord>()
                    .Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode)
                    .ToListAsync();
                // è¿™é‡Œå¯ä»¥æ ¹æ®éœ€è¦æ›´æ–°æ‹£é€‰è®°å½•的状态字段
                // ä¾‹å¦‚:pickingRecord.Status = (int)PickingStatusEnum.已完成;
                _logger.LogInformation($"找到{pickingRecords.Count}条拣选记录 - è®¢å•: {orderNo}, æ‰˜ç›˜: {palletCode}");
            }
            catch (Exception ex)
            {
                _logger.LogWarning($"更新拣选记录状态失败: {ex.Message}");
            }
        }
        #endregion
        #region è¾…助方法
        /// <summary>
        /// ç»Ÿä¸€åˆ†æžæ‰˜ç›˜çŠ¶æ€ - è¿”回托盘的完整状态信息
        /// </summary>
        private async Task<PalletStatusAnalysis> AnalyzePalletStatus(string orderNo, string palletCode, int stockId)
        {
            var result = new PalletStatusAnalysis
            {
                OrderNo = orderNo,
                PalletCode = palletCode,
                StockId = stockId
            };
            // åˆ†æžæœªåˆ†æ‹£çš„出库锁定记录
            var remainingLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(it => it.OrderNo == orderNo &&
                           it.PalletCode == palletCode &&
                           it.Status == (int)OutLockStockStatusEnum.出库中)
                .ToListAsync();
            if (remainingLocks.Any())
            {
                result.HasRemainingLocks = true;
                result.RemainingLocks = remainingLocks;
                result.RemainingLocksReturnQty = remainingLocks.Sum(x => x.AssignQuantity - x.PickedQty);
                _logger.LogInformation($"发现{remainingLocks.Count}条未分拣锁定记录,总数量: {result.RemainingLocksReturnQty}");
            }
            // åˆ†æžæ‰˜ç›˜ä¸Šçš„库存货物
            var palletStockGoods = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .Where(it => it.StockId == stockId &&
                     (it.Status == StockStatusEmun.入库确认.ObjToInt() ||
                      it.Status == StockStatusEmun.入库完成.ObjToInt() ||
                      it.Status == StockStatusEmun.出库锁定.ObjToInt()))
                .Where(it => it.StockQuantity > 0)
                .ToListAsync();
            if (palletStockGoods.Any())
            {
                result.HasPalletStockGoods = true;
                result.PalletStockGoods = palletStockGoods;
                result.PalletStockReturnQty = palletStockGoods.Sum(x => x.StockQuantity);
                _logger.LogInformation($"发现{palletStockGoods.Count}个库存货物,总数量: {result.PalletStockReturnQty}");
                // è®°å½•详细状态分布
                var statusGroups = palletStockGoods.GroupBy(x => x.Status);
                foreach (var group in statusGroups)
                {
                    _logger.LogInformation($"库存状态{group.Key}: {group.Count()}个货物,数量: {group.Sum(x => x.StockQuantity)}");
                }
            }
            //分析拆包记录
            var splitRecords = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
                .Where(it => it.OrderNo == orderNo &&
                           it.PalletCode == palletCode &&
                           !it.IsReverted && it.Status != (int)SplitPackageStatusEnum.已拣选 &&
                           it.Status != (int)SplitPackageStatusEnum.已回库)
                .ToListAsync();
            if (splitRecords.Any())
            {
                result.HasSplitRecords = true;
                result.SplitRecords = splitRecords;
                result.SplitReturnQty = await CalculateSplitReturnQuantity(splitRecords, stockId);
                _logger.LogInformation($"发现{splitRecords.Count}条未拣选拆包记录,总数量: {result.SplitReturnQty}");
            }
            // 4. è®¡ç®—总回库数量和空托盘状态
            result.TotalReturnQty = result.RemainingLocksReturnQty + result.PalletStockReturnQty + result.SplitReturnQty;
            result.HasItemsToReturn = result.TotalReturnQty > 0;
            result.IsEmptyPallet = !result.HasItemsToReturn;
            // 5. æ£€æŸ¥æ˜¯å¦æœ‰è¿›è¡Œä¸­çš„任务
            result.HasActiveTasks = await _taskRepository.Db.Queryable<Dt_Task>()
                .Where(x => x.OrderNo == orderNo && x.TaskType == TaskTypeEnum.InPick.ObjToInt() &&
                           x.PalletCode == palletCode &&
                           x.TaskStatus == (int)TaskStatusEnum.New)
                .AnyAsync();
            _logger.LogInformation($"托盘状态分析完成 - è®¢å•: {orderNo}, æ‰˜ç›˜: {palletCode}, " +
                                  $"总回库数量: {result.TotalReturnQty}, æ˜¯å¦ç©ºæ‰˜ç›˜: {result.IsEmptyPallet}, " +
                                  $"有进行中任务: {result.HasActiveTasks}");
            return result;
        }
        /// <summary>
        /// æ£€æŸ¥æ‰˜ç›˜æ˜¯å¦ä¸ºç©º
        /// </summary>
        private async Task<bool> IsPalletEmpty(string orderNo, string palletCode)
        {
            try
            {
                // èŽ·å–åº“å­˜ä¿¡æ¯
                var stockInfo = await _stockInfoService.Db.Queryable<Dt_StockInfo>()
                    .Where(x => x.PalletCode == palletCode)
                    .FirstAsync();
                if (stockInfo == null)
                    return false;
                // ä½¿ç”¨ç»Ÿä¸€çš„状态分析
                var statusAnalysis = await AnalyzePalletStatus(orderNo, palletCode, stockInfo.Id);
                return statusAnalysis.IsEmptyPallet;
            }
            catch (Exception ex)
            {
                _logger.LogWarning($"检查托盘是否为空失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Error: {ex.Message}");
                return false;
            }
        }
        /// <summary>
        /// æ£€æŸ¥å¹¶å¤„理空托盘
        /// </summary>
        private async Task<bool> CheckAndHandleEmptyPallet(string orderNo, string palletCode)
        {
            try
            {
                // 1. èŽ·å–åº“å­˜ä¿¡æ¯
                var stockInfo = await _stockInfoService.Db.Queryable<Dt_StockInfo>()
                    .Where(x => x.PalletCode == palletCode)
                    .FirstAsync();
                if (stockInfo == null)
                {
                    _logger.LogWarning($"未找到托盘 {palletCode} çš„库存信息");
                    return false;
                }
                // 2. ä½¿ç”¨ç»Ÿä¸€çš„状态分析
                var statusAnalysis = await AnalyzePalletStatus(orderNo, palletCode, stockInfo.Id);
                // 3. æ£€æŸ¥æ˜¯å¦ä¸ºç©ºæ‰˜ç›˜ä¸”没有进行中的任务
                if (!statusAnalysis.IsEmptyPallet || statusAnalysis.HasActiveTasks)
                {
                    return false;
                }
                _logger.LogInformation($"检测到空托盘,开始自动处理 - è®¢å•: {orderNo}, æ‰˜ç›˜: {palletCode}");
                //// æ¸…理零库存数据
                //await CleanupZeroStockData(stockInfo.Id);
                //// æ›´æ–°åº“存主表状态为空托盘
                //await UpdateStockInfoAsEmpty(stockInfo);
                //// å¤„理出库锁定记录
                //await HandleOutStockLockRecords(orderNo, palletCode);
                //// å¤„理任务状态
                //await HandleTaskStatusForEmptyPallet(orderNo, palletCode);
                //// æ›´æ–°è®¢å•数据
                //await UpdateOrderDataForEmptyPallet(orderNo, palletCode);
                ////记录操作历史
                //await RecordAutoEmptyPalletOperation(orderNo, palletCode);
                _logger.LogInformation($"空托盘自动处理完成 - è®¢å•: {orderNo}, æ‰˜ç›˜: {palletCode}");
                return true;
            }
            catch (Exception ex)
            {
                _logger.LogError($"自动处理空托盘失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Error: {ex.Message}");
                return false;
            }
        }
        private async Task<string> GenerateNewBarcode()
        {
@@ -1507,10 +2275,12 @@
                OriginalLockQuantity = quantity,
                IsSplitted = 1,
                ParentLockId = originalLock.Id,
                Operator=  App.User.UserName,
                FactoryArea=originalLock.FactoryArea,
                lineNo=originalLock.lineNo,
                WarehouseCode=originalLock.WarehouseCode,
                Operator = App.User.UserName,
                FactoryArea = originalLock.FactoryArea,
                lineNo = originalLock.lineNo,
                WarehouseCode = originalLock.WarehouseCode,
                BarcodeQty=originalLock.BarcodeQty,
                BarcodeUnit=originalLock.BarcodeUnit,
            };
@@ -1615,6 +2385,7 @@
            }
            return WebResponseContent.Instance.OK("拣选确认成功", new { SplitResults = new List<SplitResult>() });
        }
        #region è™šæ‹Ÿå‡ºå…¥åº“
@@ -1871,6 +2642,56 @@
        public List<Dt_StockInfoDetail> PalletStockGoods { get; set; } = new List<Dt_StockInfoDetail>();
        public List<Dt_SplitPackageRecord> SplitRecords { get; set; } = new List<Dt_SplitPackageRecord>();
    }
    public class PalletStatusAnalysis
    {
        public string OrderNo { get; set; }
        public string PalletCode { get; set; }
        public int StockId { get; set; }
        // å›žåº“相关属性
        public bool HasItemsToReturn { get; set; }
        public bool HasRemainingLocks { get; set; }
        public bool HasPalletStockGoods { get; set; }
        public bool HasSplitRecords { get; set; }
        public decimal RemainingLocksReturnQty { get; set; }
        public decimal PalletStockReturnQty { get; set; }
        public decimal SplitReturnQty { get; set; }
        public decimal TotalReturnQty { get; set; }
        public List<Dt_OutStockLockInfo> RemainingLocks { get; set; } = new List<Dt_OutStockLockInfo>();
        public List<Dt_StockInfoDetail> PalletStockGoods { get; set; } = new List<Dt_StockInfoDetail>();
        public List<Dt_SplitPackageRecord> SplitRecords { get; set; } = new List<Dt_SplitPackageRecord>();
        // ç©ºæ‰˜ç›˜ç›¸å…³å±žæ€§
        public bool IsEmptyPallet { get; set; }
        public bool HasActiveTasks { get; set; }
        // ä¾¿åˆ©æ–¹æ³•
        public bool CanReturn => HasItemsToReturn && !HasActiveTasks;
        public bool CanRemove => IsEmptyPallet && !HasActiveTasks;
    }
    public class PickingContext
    {
        public string OrderNo { get; set; }
        public string PalletCode { get; set; }
        public string Barcode { get; set; }
        public string Operator { get; set; }
        public Dt_OutStockLockInfo LockInfo { get; set; }
        public Dt_OutboundOrderDetail OrderDetail { get; set; }
        public Dt_StockInfoDetail StockDetail { get; set; }
        public decimal ActualQuantity { get; set; }
        public string AdjustedReason { get; set; }
    }
    public class CancelPickingContext
    {
        public string OrderNo { get; set; }
        public string PalletCode { get; set; }
        public string Barcode { get; set; }
        public string Operator { get; set; }
        public decimal CancelQuantity { get; set; }
        public Dt_PickingRecord PickingRecord { get; set; }
        public Dt_OutStockLockInfo LockInfo { get; set; }
        public Dt_OutboundOrderDetail OrderDetail { get; set; }
    }
    #endregion
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_OutboundService/WIDESEA_OutboundService.csproj
@@ -7,6 +7,8 @@
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\WIDESEA_BasicService\WIDESEA_BasicService.csproj" />
    <ProjectReference Include="..\WIDESEA_IAllocateService\WIDESEA_IAllocateService.csproj" />
    <ProjectReference Include="..\WIDESEA_IBasicService\WIDESEA_IBasicService.csproj" />
    <ProjectReference Include="..\WIDESEA_IInboundService\WIDESEA_IInboundService.csproj" />
    <ProjectReference Include="..\WIDESEA_IOutboundService\WIDESEA_IOutboundService.csproj" />
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs
@@ -22,6 +22,7 @@
        public IRepository<Dt_StockInfo> Repository => BaseDal;
        private readonly IRepository<Dt_StockInfoDetail> _stockInfoDetailRepository;
        private readonly IOutboundOrderService _outboundOrderService;
        //private readonly IOutboundOrderDetailService _outboundOrderDetailService;
        private readonly ILocationInfoService _locationInfoService;
        public StockInfoService(IRepository<Dt_StockInfo> BaseDal, IMapper mapper, IRepository<Dt_StockInfoDetail> stockInfoDetailRepository, IRecordService recordService, ILocationInfoService locationInfoService, IOutboundOrderService outboundOrderService) : base(BaseDal)
        {
@@ -30,6 +31,7 @@
            _recordService = recordService;
            _locationInfoService = locationInfoService;
            _outboundOrderService = outboundOrderService;
            //_outboundOrderDetailService = outboundOrderDetailService;
        }
        /// <summary>
@@ -201,12 +203,9 @@
            if (!string.IsNullOrEmpty(supplyCode))
            {
                query = query.Where(x => x.Details.Any(d => d.SupplyCode == supplyCode));
            }
            }
            var stocks = query.ToList();
            return stocks.OrderBy(x => x.Details.Where(d => d.MaterielCode == materielCode && (string.IsNullOrEmpty(supplyCode) || d.SupplyCode == supplyCode) &&
                           (string.IsNullOrEmpty(lotNo) || d.BatchNo == lotNo)).Min(d => d.CreateDate)).ToList();
            return query.OrderBy(x => x.CreateDate).ToList();
            //ISugarQueryable<Dt_LocationInfo> sugarQueryable = Db.Queryable<Dt_LocationInfo>().Where(x => locationCodes.Contains(x.LocationCode));
            //ISugarQueryable<Dt_StockInfo> sugarQueryable1 = Db.Queryable<Dt_StockInfo>().Includes(x => x.Details).Where(x => x.Details.Any(v => v.MaterielCode == materielCode));
            //return sugarQueryable.InnerJoin(sugarQueryable1, (a, b) => a.LocationCode == b.LocationCode).Select((a, b) => b).OrderBy(a => a.CreateDate).Includes(a => a.Details).ToList();
@@ -227,49 +226,133 @@
        public List<Dt_StockInfo> GetStockInfosByPalletCodes(List<string> palletCodes)
        {
            return Db.Queryable<Dt_StockInfo>().Where(x => palletCodes.Contains(x.PalletCode)).Includes(x => x.Details).ToList();
            return Db.Queryable<Dt_StockInfo>().Where(x => palletCodes.Contains(x.PalletCode)).Includes(x => x.Details).OrderBy(x => x.CreateDate).ToList();
        }
        // æ·»åŠ èŽ·å–å•ä¸ªæ‰˜ç›˜åº“å­˜çš„æ–¹æ³•ï¼ˆå¦‚æžœä¸å­˜åœ¨ï¼‰
        public Dt_StockInfo GetStockInfoByPalletCode(string palletCode)
        {
            return Db.Queryable<Dt_StockInfo>()
                .Where(x => x.PalletCode == palletCode)
                .Includes(x => x.Details)
                .First();
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="orderId"></param>
        /// <param name="materielCode"></param>
        /// <returns></returns>
        //public List<StockSelectViewDTO> GetStockSelectViews(int orderId, string materielCode)
        //{
        //    try
        //    {
        //        Dt_OutboundOrder outboundOrder = _outboundOrderService.Repository.QueryFirst(x => x.Id == orderId);
        //        if (outboundOrder == null)
        //        {
        //            throw new Exception($"未找到出库单信息");
        //        }
        //        List<string> locationCodes = _locationInfoService.GetCanOutLocationCodes();
        //        return BaseDal.QueryTabs<Dt_StockInfo, Dt_StockInfoDetail, StockSelectViewDTO>((a, b) => a.Id == b.StockId, (a, b) => new StockSelectViewDTO
        //        {
        //            LocationCode = a.LocationCode,
        //            MaterielCode = b.MaterielCode,
        //            MaterielName = b.MaterielName,
        //            Barcode=b.Barcode,
        //            PalletCode = a.PalletCode,
        //            UseableQuantity = b.StockQuantity - b.OutboundQuantity
        //        }, a => locationCodes.Contains(a.LocationCode), b => b.StockQuantity > b.OutboundQuantity && b.MaterielCode == materielCode, x => true).GroupBy(x => x.PalletCode).Select(x => new StockSelectViewDTO
        //        {
        //            LocationCode = x.FirstOrDefault()?.LocationCode ?? "",
        //            MaterielCode = x.FirstOrDefault()?.MaterielCode ?? "",
        //            MaterielName = x.FirstOrDefault()?.MaterielName ?? "",
        //            Barcode=x.FirstOrDefault()?.Barcode??"",
        //            PalletCode = x.Key,
        //            UseableQuantity = x.Sum(x => x.UseableQuantity)
        //        }).ToList();
        //    }
        //    catch (Exception ex)
        //    {
        //        return null;
        //    }
        //}
        public List<StockSelectViewDTO> GetStockSelectViews(int orderId, string materielCode)
        {
            try
            Dt_OutboundOrder outboundOrder = _outboundOrderService.Repository.QueryFirst(x => x.Id == orderId);
            if (outboundOrder == null)
            {
                Dt_OutboundOrder outboundOrder = _outboundOrderService.Repository.QueryFirst(x => x.Id == orderId);
                if (outboundOrder == null)
                throw new Exception($"未找到ID为{orderId}的出库单信息");
            }
            // èŽ·å–å‡ºåº“å•æ˜Žç»†ï¼Œç”¨äºŽç¡®å®šéœ€è¦çš„æ‰¹æ¬¡å’Œä¾›åº”å•†
            var orderDetails = SqlSugarHelper.DbWMS.Queryable<Dt_OutboundOrderDetail>().Where(x => x.OrderId == orderId && x.MaterielCode == materielCode).ToList();
            if (!orderDetails.Any())
            {
                throw new Exception($"出库单{orderId}中未找到物料{materielCode}的明细信息");
            }
            List<string> locationCodes = _locationInfoService.GetCanOutLocationCodes();
            var result = new List<StockSelectViewDTO>();
            // ä¸ºæ¯ä¸ªå‡ºåº“单明细查询对应的库存
            foreach (var orderDetail in orderDetails)
            {
                // æž„建查询条件
                var stockQuery = Db.Queryable<Dt_StockInfo>()
                    .Where(x => locationCodes.Contains(x.LocationCode))
                    .Where(x => x.StockStatus == (int)StockStatusEmun.入库完成)
                    .Includes(x => x.Details);
                // æ‰§è¡ŒæŸ¥è¯¢å¹¶æŒ‰å…ˆè¿›å…ˆå‡ºæŽ’序
                var stocks = stockQuery.ToList()
                    .Where(x => x.Details.Any(d =>
                        d.MaterielCode == materielCode &&
                        string.IsNullOrEmpty(orderDetail.BatchNo)?true: d.BatchNo == orderDetail.BatchNo&&
                        string.IsNullOrEmpty(orderDetail.BatchNo)?true:d.SupplyCode == orderDetail.SupplyCode &&
                        d.StockQuantity > d.OutboundQuantity
                    ))
                    .OrderBy(x => x.CreateDate)
                    .ToList();
                foreach (var stock in stocks)
                {
                    throw new Exception($"未找到出库单信息");
                    var relevantDetails = stock.Details
                        .Where(d => d.MaterielCode == materielCode &&
                                   string.IsNullOrEmpty(orderDetail.BatchNo) ? true : d.BatchNo == orderDetail.BatchNo &&
                                   string.IsNullOrEmpty(orderDetail.BatchNo) ? true : d.SupplyCode == orderDetail.SupplyCode &&
                                   d.StockQuantity > d.OutboundQuantity)
                        .ToList();
                    if (relevantDetails.Any())
                    {
                        var firstDetail = relevantDetails.First();
                        var useableQuantity = relevantDetails.Sum(d => d.StockQuantity - d.OutboundQuantity);
                        result.Add(new StockSelectViewDTO
                        {
                            LocationCode = stock.LocationCode,
                            MaterielCode = materielCode,
                            MaterielName = firstDetail.MaterielName,
                            BatchNo = orderDetail.BatchNo,
                            SupplyCode = orderDetail.SupplyCode,
                            Barcode = firstDetail.Barcode,
                            PalletCode = stock.PalletCode,
                            UseableQuantity = useableQuantity,
                            StockCreateDate = stock.CreateDate,
                            StockId = stock.Id,
                            OrderDetailId = orderDetail.Id // å…³è”到具体的出库单明细
                        });
                    }
                }
                List<string> locationCodes = _locationInfoService.GetCanOutLocationCodes();
                return BaseDal.QueryTabs<Dt_StockInfo, Dt_StockInfoDetail, StockSelectViewDTO>((a, b) => a.Id == b.StockId, (a, b) => new StockSelectViewDTO
                {
                    LocationCode = a.LocationCode,
                    MaterielCode = b.MaterielCode,
                    MaterielName = b.MaterielName,
                    PalletCode = a.PalletCode,
                    UseableQuantity = b.StockQuantity - b.OutboundQuantity
                }, a => locationCodes.Contains(a.LocationCode), b => b.StockQuantity > b.OutboundQuantity && b.MaterielCode == materielCode, x => true).GroupBy(x => x.PalletCode).Select(x => new StockSelectViewDTO
                {
                    LocationCode = x.FirstOrDefault()?.LocationCode ?? "",
                    MaterielCode = x.FirstOrDefault()?.MaterielCode ?? "",
                    MaterielName = x.FirstOrDefault()?.MaterielName ?? "",
                    PalletCode = x.Key,
                    UseableQuantity = x.Sum(x => x.UseableQuantity)
                }).ToList();
            }
            catch (Exception ex)
            {
                return null;
            }
            return result;
        }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs
@@ -41,6 +41,7 @@
using WIDESEA_DTO.Allocate;
using WIDESEA_DTO.Basic;
using WIDESEA_DTO.Inbound;
using WIDESEA_DTO.Outbound;
using WIDESEA_DTO.Task;
using WIDESEA_IAllocateService;
using WIDESEA_IBasicService;
@@ -50,6 +51,7 @@
using WIDESEA_IStockService;
using WIDESEA_ITaskInfoService;
using WIDESEA_Model.Models;
using WIDESEA_Model.Models.Outbound;
namespace WIDESEA_TaskInfoService
{
@@ -64,6 +66,7 @@
        private readonly IInboundOrderService _inboundOrderService;
        private readonly IInboundOrderDetailService _inboundOrderDetailService;
        private readonly IRepository<Dt_OutboundBatch> _OutboundBatchRepository;
        private readonly IOutboundOrderService _outboundOrderService;
        private readonly IOutboundOrderDetailService _outboundOrderDetailService;
        private readonly IOutStockLockInfoService _outStockLockInfoService;
@@ -92,7 +95,7 @@
        public List<int> TaskOutboundTypes => typeof(TaskTypeEnum).GetEnumIndexList();
        public TaskService(IRepository<Dt_Task> BaseDal, IMapper mapper, IUnitOfWorkManage unitOfWorkManage, IRepository<Dt_StockInfo> stockRepository, ILocationInfoService locationInfoService, IInboundOrderService inboundOrderService, ILocationStatusChangeRecordService locationStatusChangeRecordService, IESSApiService eSSApiService, ILogger<TaskService> logger, IStockService stockService, IRecordService recordService, IInboundOrderDetailService inboundOrderDetailService, IOutboundOrderService outboundOrderService, IOutboundOrderDetailService outboundOrderDetailService, IInvokeMESService invokeMESService, IOutStockLockInfoService outStockLockInfoService, IAllocateService allocateService) : base(BaseDal)
        public TaskService(IRepository<Dt_Task> BaseDal, IMapper mapper, IUnitOfWorkManage unitOfWorkManage, IRepository<Dt_StockInfo> stockRepository, ILocationInfoService locationInfoService, IInboundOrderService inboundOrderService, ILocationStatusChangeRecordService locationStatusChangeRecordService, IESSApiService eSSApiService, ILogger<TaskService> logger, IStockService stockService, IRecordService recordService, IInboundOrderDetailService inboundOrderDetailService, IOutboundOrderService outboundOrderService, IOutboundOrderDetailService outboundOrderDetailService, IInvokeMESService invokeMESService, IOutStockLockInfoService outStockLockInfoService, IAllocateService allocateService, IRepository<Dt_OutboundBatch> outboundBatchRepository) : base(BaseDal)
        {
            _mapper = mapper;
            _unitOfWorkManage = unitOfWorkManage;
@@ -110,6 +113,7 @@
            _invokeMESService = invokeMESService;
            _outStockLockInfoService = outStockLockInfoService;
            _allocateService = allocateService;
            _OutboundBatchRepository = outboundBatchRepository;
        }
@@ -126,7 +130,7 @@
                if (int.TryParse(taskNum, out var newTaskNum))
                {
                    task = BaseDal.QueryFirst(x => x.TaskNum == newTaskNum);
                    task = await BaseDal.QueryFirstAsync(x => x.TaskNum == newTaskNum);
                    if (task == null)
                    {
                        return WebResponseContent.Instance.Error("未找到任务信息");
@@ -141,10 +145,28 @@
                MethodInfo? methodInfo = GetType().GetMethod(((TaskTypeEnum)task.TaskType) + "TaskCompleted");
                if (methodInfo != null)
                {
                    WebResponseContent? responseContent = (WebResponseContent?)methodInfo.Invoke(this, new object[] { task });
                    if (responseContent != null)
                    object? taskResult = methodInfo.Invoke(this, new object[] { task });
                    if (taskResult is Task<WebResponseContent> asyncTask)
                    {
                        return responseContent;
                        try
                        {
                            // 3. å¼‚步等待 Task å®Œæˆï¼Œè‡ªåŠ¨è§£æžå‡º WebResponseContent
                            WebResponseContent responseContent = await asyncTask;
                            if (responseContent != null)
                            {
                                return responseContent;
                            }
                        }
                        catch (AggregateException ex)
                        {
                            _logger.LogError($"TaskService TaskCompleted  taskResult:   {ex.Message} ");
                            return WebResponseContent.Instance.Error(ex.Message);
                        }
                        catch (Exception ex)
                        {
                            _logger.LogError(ex, $"Unexpected error in {task.TaskType}");
                            return WebResponseContent.Instance.Error(ex.Message);
                        }
                    }
                }
                return WebResponseContent.Instance.Error("未找到任务类型对应业务处理逻辑");
@@ -251,12 +273,12 @@
            {
                foreach (var inboundOrder in inboundOrders)
                {
                    if (inboundOrder.OrderType == InOrderTypeEnum.Allocat.ObjToInt())//调拨入库
                    if (inboundOrder.OrderType == InOrderTypeEnum.AllocatInbound.ObjToInt())//调拨入库
                    {
                        if (inboundOrder != null && inboundOrder.OrderStatus == InOrderStatusEnum.入库完成.ObjToInt())
                        {
                            var allocate = _allocateService.Repository.QueryData(x => x.OrderNo == inboundOrder.InboundOrderNo).First();
                            var feedmodel = new AllocateDto
                            var allocatefeedmodel = new AllocateDto
                            {
                                ReqCode = Guid.NewGuid().ToString(),
                                ReqTime = DateTime.Now.ToString(),
@@ -265,8 +287,8 @@
                                OperationType = 1,
                                Operator = inboundOrder.Operator,
                                OrderNo = inboundOrder.UpperOrderNo,
                                fromWarehouse = allocate?.FromWarehouse??"",
                                toWarehouse = allocate?.ToWarehouse??"",
                                fromWarehouse = allocate?.FromWarehouse ?? "",
                                toWarehouse = allocate?.ToWarehouse ?? "",
                                Details = new List<AllocateDtoDetail>()
                            };
@@ -289,9 +311,9 @@
                                       Unit = row.Unit
                                   }).ToList()
                               }).ToList();
                            feedmodel.Details = groupedData;
                            allocatefeedmodel.Details = groupedData;
                            var result = await _invokeMESService.FeedbackAllocate(feedmodel);
                            var result = await _invokeMESService.FeedbackAllocate(allocatefeedmodel);
                            if (result != null && result.code == 200)
                            {
                                _inboundOrderService.Db.Updateable<Dt_InboundOrder>().SetColumns(it => new Dt_InboundOrder { ReturnToMESStatus = 1 })
@@ -363,7 +385,14 @@
            return WebResponseContent.Instance.OK();
        }
        public WebResponseContent OutboundTaskCompleted(Dt_Task task)
        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}");
            //查货位
@@ -375,7 +404,7 @@
            locationInfo.LocationStatus = LocationStatusEnum.Free.ObjToInt();
            _locationInfoService.Repository.UpdateData(locationInfo);
            var outloks = _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>().Where(x => x.TaskNum == task.TaskNum).ToList();
            var outloks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>().Where(x => x.TaskNum == task.TaskNum).ToListAsync();
            var stockids = outloks.Select(x => x.StockId).ToList();
@@ -439,12 +468,22 @@
                stockInfo.StockStatus = StockStatusEmun.入库完成.ObjToInt();
                _stockRepository.UpdateData(stockInfo);
                var outboundOrder = _outboundOrderService.Db.Queryable<Dt_OutboundOrder>().First(x => x.OrderNo == task.OrderNo);
                task.TaskStatus = TaskStatusEnum.Finish.ObjToInt();
                BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? WIDESEA_Core.Enums.OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
                _locationStatusChangeRecordService.AddLocationStatusChangeRecord(locationInfo, beforelocationStatus, StockChangeType.Inbound.ObjToInt(), "", task.TaskNum);
                if (outboundOrder != null)
                {
                    await HandleOutboundOrderToMESCompletion(outboundOrder, outboundOrder.OrderNo);
                }
                else
                {
                    _logger.LogInformation($"TaskService  InEmptyTaskCompleted: {task.TaskNum} ,未找到出库单。  ");
                }
                return content;
            }
@@ -457,62 +496,260 @@
        public async Task<WebResponseContent> InPickTaskCompleted(Dt_Task task)
        {
            _logger.LogInformation($"TaskService  InPickTaskCompleted: {task.TaskNum}");
            //查库存
            Dt_StockInfo stockInfo = _stockRepository.Db.Queryable<Dt_StockInfo>().Includes(x => x.Details).Where(x => x.PalletCode == task.PalletCode).First();
            if (stockInfo == null)
            try
            {
                return WebResponseContent.Instance.Error($"未找到托盘对应的组盘信息");
                //查库存
                Dt_StockInfo stockInfo = await _stockRepository.Db.Queryable<Dt_StockInfo>().Includes(x => x.Details).Where(x => x.PalletCode == task.PalletCode).FirstAsync();
                if (stockInfo == null)
                {
                    _logger.LogInformation($"TaskService  InPickTaskCompleted: æœªæ‰¾åˆ°æ‰˜ç›˜å¯¹åº”的组盘信息.{task.TaskNum}");
                    return WebResponseContent.Instance.Error($"未找到托盘对应的组盘信息");
                }
                if (stockInfo.Details.Count == 0 && stockInfo.PalletType != PalletTypeEnum.Empty.ObjToInt())
                {
                    _logger.LogInformation($"TaskService  InPickTaskCompleted: æœªæ‰¾åˆ°è¯¥æ‰˜ç›˜åº“存明细信息.{task.TaskNum}");
                    return WebResponseContent.Instance.Error($"未找到该托盘库存明细信息");
                }
                //查货位
                Dt_LocationInfo locationInfo = _locationInfoService.Repository.QueryFirst(x => x.LocationCode == task.TargetAddress);
                if (locationInfo == null)
                {
                    _logger.LogInformation($"TaskService  InPickTaskCompleted:  æœªæ‰¾åˆ°å¯¹åº”的终点货位信息 {task.TaskNum}.");
                    return WebResponseContent.Instance.Error($"未找到对应的终点货位信息");
                }
                var beforelocationStatus = locationInfo.LocationStatus;
                // èŽ·å–æ‰€æœ‰å›žåº“ä¸­çš„å‡ºåº“é”å®šè®°å½•
                var returnLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                    .Where(it => it.OrderNo == task.OrderNo && it.PalletCode == task.PalletCode && it.Status == (int)OutLockStockStatusEnum.回库中)
                    .ToListAsync();
                // æ›´æ–°å‡ºåº“锁定记录状态为回库完成
                foreach (var lockInfo in returnLocks)
                {
                    lockInfo.Status = (int)OutLockStockStatusEnum.已回库;
                }
                _outStockLockInfoService.Db.Updateable(returnLocks).ExecuteCommand();
                stockInfo.LocationCode = task.TargetAddress;
                stockInfo.StockStatus = StockStatusEmun.入库完成.ObjToInt();
                if (stockInfo.Details != null && stockInfo.Details.Any())
                {
                    stockInfo.Details.ForEach(x =>
                    {
                        x.Status = StockStatusEmun.入库完成.ObjToInt();
                    });
                    _stockService.StockInfoDetailService.Repository.UpdateData(stockInfo.Details);
                }
                _stockService.StockInfoService.Repository.UpdateData(stockInfo);
                await ProcessStockDetailsForReturn(task, stockInfo.Id);
                await DeleteZeroQuantityStockDetails(stockInfo.Id);
                if (stockInfo.PalletType == PalletTypeEnum.Empty.ObjToInt())
                {
                    locationInfo.LocationStatus = LocationStatusEnum.Pallet.ObjToInt();
                }
                else
                {
                    locationInfo.LocationStatus = LocationStatusEnum.InStock.ObjToInt();
                }
                _locationInfoService.Repository.UpdateData(locationInfo);
                var outboundOrder = _outboundOrderService.Db.Queryable<Dt_OutboundOrder>().First(x => x.OrderNo == task.OrderNo);
                task.TaskStatus = TaskStatusEnum.Finish.ObjToInt();
                BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
                BaseDal.DeleteData(task);
                _locationStatusChangeRecordService.AddLocationStatusChangeRecord(locationInfo, beforelocationStatus, StockChangeType.Inbound.ObjToInt(), "", task.TaskNum);
                if (outboundOrder != null)
                {
                    await HandleOutboundOrderToMESCompletion(outboundOrder, outboundOrder.OrderNo);
                }
                else
                {
                    _logger.LogInformation($"TaskService  InPickTaskCompleted: {task.TaskNum} ,未找到出库单。  ");
                }
            }
            if (stockInfo.Details.Count == 0 && stockInfo.PalletType != PalletTypeEnum.Empty.ObjToInt())
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"未找到该托盘库存明细信息");
                _logger.LogInformation($"TaskService  InPickTaskCompleted: {task.TaskNum} , {ex.Message}");
            }
            //查货位
            Dt_LocationInfo locationInfo = _locationInfoService.Repository.QueryFirst(x => x.LocationCode == task.TargetAddress);
            if (locationInfo == null)
            return await Task.FromResult(WebResponseContent.Instance.OK());
        }
        private async Task HandleOutboundOrderToMESCompletion(Dt_OutboundOrder outboundOrder, string orderNo)
        {
            try
            {
                return WebResponseContent.Instance.Error($"未找到对应的终点货位信息");
                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)
                    .ToListAsync();
                bool allCompleted = true;
                foreach (var detail in orderDetails)
                {
                    if (detail.OverOutQuantity < detail.NeedOutQuantity)
                    {
                        allCompleted = false;
                        break;
                    }
                }
                _logger.LogInformation($"TaskService  HandleOutboundOrderToMESCompletion: {outboundOrder.OrderNo} , {allCompleted}");
                int newStatus = allCompleted ? (int)OutOrderStatusEnum.出库完成 : (int)OutOrderStatusEnum.出库中;
                if (outboundOrder.OrderStatus != newStatus)
                {
                    await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
                        .SetColumns(x => x.OrderStatus == newStatus)
                        .Where(x => x.OrderNo == orderNo)
                        .ExecuteCommandAsync();
                }
                //只有正常分拣完成时才向MES反馈
                if (allCompleted && newStatus == (int)OutOrderStatusEnum.出库完成)
                {
                    if (outboundOrder.OrderType == OutOrderTypeEnum.Allocate.ObjToInt())
                    {
                        var allocate = _allocateService.Repository.QueryData(x => x.UpperOrderNo == outboundOrder.UpperOrderNo).First();
                        var allocatefeedmodel = new AllocateDto
                        {
                            ReqCode = Guid.NewGuid().ToString(),
                            ReqTime = DateTime.Now.ToString(),
                            BusinessType = "3",
                            FactoryArea = outboundOrder.FactoryArea,
                            OperationType = 1,
                            Operator = outboundOrder.Operator,
                            OrderNo = outboundOrder.UpperOrderNo,
                            // documentsNO = outboundOrder.OrderNo,
                            // status = outboundOrder.OrderStatus,
                            fromWarehouse = allocate?.FromWarehouse ?? "",
                            toWarehouse = allocate?.ToWarehouse ?? "",
                            Details = new List<AllocateDtoDetail>()
                        };
                        foreach (var detail in orderDetails)
                        {
                            // èŽ·å–è¯¥æ˜Žç»†å¯¹åº”çš„æ¡ç ä¿¡æ¯ï¼ˆä»Žé”å®šè®°å½•ï¼‰
                            var detailLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                                .Where(x => x.OrderNo == orderNo &&
                                           x.OrderDetailId == detail.Id &&
                                           x.Status == (int)OutLockStockStatusEnum.拣选完成)
                                .ToListAsync();
                            var detailModel = new AllocateDtoDetail
                            {
                                MaterialCode = detail.MaterielCode,
                                LineNo = detail.lineNo, // æ³¨æ„ï¼šè¿™é‡Œå¯èƒ½éœ€è¦è°ƒæ•´å­—段名
                                WarehouseCode = detail.WarehouseCode,
                                Qty = detail.OverOutQuantity, // ä½¿ç”¨è®¢å•明细的已出库数量
                                //currentDeliveryQty = detail.OverOutQuantity,
                                Unit = detail.Unit,
                                Barcodes = detailLocks.Select(lockInfo => new BarcodeInfo
                                {
                                    Barcode = lockInfo.CurrentBarcode,
                                    SupplyCode = lockInfo.SupplyCode,
                                    BatchNo = lockInfo.BatchNo,
                                    Unit = lockInfo.Unit,
                                    Qty = lockInfo.PickedQty // æ¡ç çº§åˆ«çš„æ•°é‡ä»ç”¨é”å®šè®°å½•
                                }).ToList()
                            };
                            allocatefeedmodel.Details.Add(detailModel);
                        }
                        var result = await _invokeMESService.FeedbackAllocate(allocatefeedmodel);
                        if (result != null && result.code == 200)
                        {
                            await _outboundOrderDetailService.Db.Updateable<Dt_OutboundOrderDetail>()
                                   .SetColumns(x => x.ReturnToMESStatus == 1)
                                   .Where(x => x.OrderId == outboundOrder.Id).ExecuteCommandAsync();
                            await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
                                  .SetColumns(x => new Dt_OutboundOrder
                                  {
                                      ReturnToMESStatus = 1,
                                      Operator = App.User.UserName,
                                  }).Where(x => x.OrderNo == orderNo).ExecuteCommandAsync();
                        }
                    }
                    else if (outboundOrder.OrderType == OutOrderTypeEnum.ReCheck.ObjToInt())
                    {
                    }
                    else
                    {
                        var feedmodel = new FeedbackOutboundRequestModel
                        {
                            reqCode = Guid.NewGuid().ToString(),
                            reqTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
                            business_type = outboundOrder.BusinessType,
                            factoryArea = outboundOrder.FactoryArea,
                            operationType = 1,
                            Operator = outboundOrder.Operator,
                            orderNo = outboundOrder.UpperOrderNo,
                            documentsNO = outboundOrder.OrderNo,
                            status = outboundOrder.OrderStatus,
                            details = new List<FeedbackOutboundDetailsModel>()
                        };
                        foreach (var detail in orderDetails)
                        {
                            // èŽ·å–è¯¥æ˜Žç»†å¯¹åº”çš„æ¡ç ä¿¡æ¯ï¼ˆä»Žé”å®šè®°å½•ï¼‰
                            var detailLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                                .Where(x => x.OrderNo == orderNo &&
                                           x.OrderDetailId == detail.Id &&
                                           x.Status == (int)OutLockStockStatusEnum.拣选完成)
                                .ToListAsync();
                            var detailModel = new FeedbackOutboundDetailsModel
                            {
                                materialCode = detail.MaterielCode,
                                lineNo = detail.lineNo, // æ³¨æ„ï¼šè¿™é‡Œå¯èƒ½éœ€è¦è°ƒæ•´å­—段名
                                warehouseCode = detail.WarehouseCode,
                                qty = detail.OverOutQuantity, // ä½¿ç”¨è®¢å•明细的已出库数量
                                currentDeliveryQty = detail.OverOutQuantity,
                                unit = detail.Unit,
                                barcodes = detailLocks.Select(lockInfo => new WIDESEA_DTO.Outbound.BarcodesModel
                                {
                                    barcode = lockInfo.CurrentBarcode,
                                    supplyCode = lockInfo.SupplyCode,
                                    batchNo = lockInfo.BatchNo,
                                    unit = lockInfo.Unit,
                                    qty = lockInfo.PickedQty // æ¡ç çº§åˆ«çš„æ•°é‡ä»ç”¨é”å®šè®°å½•
                                }).ToList()
                            };
                            feedmodel.details.Add(detailModel);
                        }
                        var result = await _invokeMESService.FeedbackOutbound(feedmodel);
                        if (result != null && result.code == 200)
                        {
                            await _outboundOrderDetailService.Db.Updateable<Dt_OutboundOrderDetail>()
                                .SetColumns(x => x.ReturnToMESStatus == 1)
                                .Where(x => x.OrderId == outboundOrder.Id)
                                .ExecuteCommandAsync();
                            await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
                                .SetColumns(x => x.ReturnToMESStatus == 1)
                                .Where(x => x.OrderNo == orderNo)
                                .ExecuteCommandAsync();
                        }
                    }
                }
            }
            // èŽ·å–æ‰€æœ‰å›žåº“ä¸­çš„å‡ºåº“é”å®šè®°å½•
            var returnLocks = _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(it => it.OrderNo == task.OrderNo && it.PalletCode == task.PalletCode && it.Status == (int)OutLockStockStatusEnum.回库中)
                .ToList();
            // æ›´æ–°å‡ºåº“锁定记录状态为回库完成
            foreach (var lockInfo in returnLocks)
            catch (Exception ex)
            {
                lockInfo.Status = (int)OutLockStockStatusEnum.已回库;
                _logger.LogError($"CheckAndUpdateOrderStatus失败 - OrderNo: {orderNo}, Error: {ex.Message}");
            }
            _outStockLockInfoService.Db.Updateable(returnLocks).ExecuteCommand();
            await DeleteZeroQuantityStockDetails(stockInfo.Id);
            stockInfo.LocationCode = task.TargetAddress;
            stockInfo.StockStatus = StockStatusEmun.入库完成.ObjToInt();
            stockInfo.Details.ForEach(x =>
            {
                x.Status = StockStatusEmun.入库完成.ObjToInt();
            });
            _stockService.StockInfoService.Repository.UpdateData(stockInfo);
            _stockService.StockInfoDetailService.Repository.UpdateData(stockInfo.Details);
            await ProcessStockDetailsForReturn(task, stockInfo.Id);
            if (stockInfo.PalletType == PalletTypeEnum.Empty.ObjToInt())
            {
                locationInfo.LocationStatus = LocationStatusEnum.Pallet.ObjToInt();
            }
            else
            {
                locationInfo.LocationStatus = LocationStatusEnum.InStock.ObjToInt();
            }
            _locationInfoService.Repository.UpdateData(locationInfo);
            task.TaskStatus = TaskStatusEnum.Finish.ObjToInt();
            BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
            return WebResponseContent.Instance.OK();
        }
        /// <summary>
@@ -526,7 +763,7 @@
                var deleteCount = await _stockService.StockInfoDetailService.Db.Deleteable<Dt_StockInfoDetail>()
                    .Where(x => x.StockId == stockId &&
                               x.StockQuantity == 0 &&
                               (x.Status==StockStatusEmun.出库完成.ObjToInt()|| x.Status==
                               (x.Status == StockStatusEmun.出库完成.ObjToInt() || x.Status ==
                                          StockStatusEmun.入库完成.ObjToInt())) // åªåˆ é™¤å·²å®ŒæˆçŠ¶æ€çš„é›¶åº“å­˜
                    .ExecuteCommandAsync();
@@ -550,14 +787,14 @@
            var stockDetails = await _stockService.StockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .Where(x => x.StockId == stockId &&
                           x.StockQuantity > 0 &&
                          ( x.Status == StockStatusEmun.出库锁定.ObjToInt()|| x.Status==
                          (x.Status == StockStatusEmun.出库锁定.ObjToInt() || x.Status ==
                                      StockStatusEmun.入库确认.ObjToInt()))  // åŒ…括出库锁定和入库确认的
                .ToListAsync();
            foreach (var detail in stockDetails)
            {
                detail.Status = StockStatusEmun.入库完成.ObjToInt();
                detail.Status = StockStatusEmun.入库完成.ObjToInt();
                detail.OutboundQuantity = 0;  // æ¸…空出库数量       
                _logger.LogInformation($"更新库存明细状态 - æ¡ç : {detail.Barcode}, æ•°é‡: {detail.StockQuantity}");
@@ -597,11 +834,11 @@
                task.TaskStatus = TaskStatusEnum.Finish.ObjToInt();
                BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
                _stockService.StockInfoService.Repository.DeleteAndMoveIntoHty(stockInfo, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
                _stockRepository.Db.Deleteable(stockInfo).ExecuteCommand();
                //_stockRepository.Db.Deleteable(stockInfo).ExecuteCommand();
                _stockService.StockInfoService.DeleteData(stockInfo);
                _locationStatusChangeRecordService.AddLocationStatusChangeRecord(locationInfo, beforeStatus, StockChangeType.Outbound.ObjToInt(), stockInfo.Details.FirstOrDefault()?.OrderNo ?? "", task.TaskNum);
                return WebResponseContent.Instance.OK();
                return await Task.FromResult(WebResponseContent.Instance.OK());
            }
            catch (Exception ex)
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_Inbound.cs
@@ -29,10 +29,10 @@
        {
            try
            {
                Dt_Task dbtask = Repository.QueryFirst(x => x.PalletCode == palletCode);
                Dt_Task dbtask = Repository.Db.Queryable<Dt_Task>().Where(x => x.PalletCode == palletCode).OrderByDescending(x=>x.CreateDate).First();
                if (dbtask != null)
                {
                    if (dbtask.TaskType == TaskTypeEnum.Outbound.ObjToInt())
                    if (dbtask.TaskType == TaskTypeEnum.Outbound.ObjToInt() || dbtask.TaskType == TaskTypeEnum.OutAllocate.ObjToInt())
                    {
                        return WebResponseContent.Instance.Error($"出库待分拣任务");
                    }
@@ -40,7 +40,7 @@
                    {
                        return WebResponseContent.Instance.Error($"该托盘已生成任务");
                    }
                }
                }
                Dt_StockInfo stockInfo = _stockRepository.Db.Queryable<Dt_StockInfo>().Where(x => x.PalletCode == palletCode).Includes(x => x.Details).First();
                if (stockInfo == null)
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_Outbound.cs
@@ -6,6 +6,7 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WIDESEA_BasicService;
using WIDESEA_Common.CommonEnum;
using WIDESEA_Common.LocationEnum;
using WIDESEA_Common.OrderEnum;
@@ -17,6 +18,8 @@
using WIDESEA_DTO.Basic;
using WIDESEA_DTO.Stock;
using WIDESEA_Model.Models;
using WIDESEA_Model.Models.Basic;
using WIDESEA_Model.Models.Outbound;
namespace WIDESEA_TaskInfoService
{
@@ -140,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)
@@ -182,10 +193,34 @@
            else
            {
                throw new Exception("无库存");
            }
            }
            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>
        /// ç”Ÿæˆå‡ºåº“任务后数据更新到数据库
@@ -206,7 +241,7 @@
                if (stockInfos != null && stockInfos.Count > 0 && outboundOrderDetails != null && outboundOrderDetails.Count > 0 && outStockLockInfos != null && outStockLockInfos.Count > 0 && locationInfos != null && locationInfos.Count > 0)
                {
                    stockInfos.ForEach(x =>
                    {
                    {
                        x.StockStatus = StockStatusEmun.出库锁定.ObjToInt();
                    });
                    outboundOrderDetails.ForEach(x =>
@@ -218,7 +253,11 @@
                    {
                        _outboundOrderService.Repository.UpdateData(outboundOrder);
                    }
                    outboundOrder.Operator = App.User.UserName;
                    else
                    {
                        outboundOrder.OrderStatus = OutOrderStatusEnum.出库中.ObjToInt();
                    }
                    outboundOrder.Operator = App.User.UserName;
                    _outboundOrderService.Repository.UpdateData(outboundOrder);
                    WebResponseContent content = _outboundOrderDetailService.LockOutboundStockDataUpdate(stockInfos, outboundOrderDetails, outStockLockInfos, locationInfos, tasks: tasks);
@@ -239,6 +278,12 @@
                    {
                        _outboundOrderService.Repository.UpdateData(outboundOrder);
                    }
                    else
                    {
                        outboundOrder.OrderStatus = OutOrderStatusEnum.出库中.ObjToInt();
                    }
                    outboundOrder.Operator = App.User.UserName;
                    _outboundOrderService.Repository.UpdateData(outboundOrder);
                    _outboundOrderDetailService.Repository.UpdateData(outboundOrderDetails);
                }
                _unitOfWorkManage.CommitTran();
@@ -471,10 +516,10 @@
                throw new Exception("未找到出库单明细信息");
            }
            if (stockSelectViews.Sum(x => x.UseableQuantity) > outboundOrderDetail.OrderQuantity - outboundOrderDetail.LockQuantity)
            {
                throw new Exception("选择数量超出单据数量");
            }
            //if (stockSelectViews.Sum(x => x.UseableQuantity) > outboundOrderDetail.OrderQuantity - outboundOrderDetail.LockQuantity)
            //{
            //    throw new Exception("选择数量超出单据数量");
            //}
            List<Dt_StockInfo>? stockInfos = null;
            Dt_OutboundOrderDetail? orderDetail = null;
            List<Dt_OutStockLockInfo>? outStockLockInfos = null;
@@ -484,7 +529,7 @@
                (List<Dt_StockInfo>, Dt_OutboundOrderDetail, List<Dt_OutStockLockInfo>, List<Dt_LocationInfo>) result = _outboundOrderDetailService.AssignStockOutbound(outboundOrderDetail, stockSelectViews);
                if (result.Item1 != null && result.Item1.Count > 0)
                {
                    Dt_OutboundOrder outboundOrder = _outboundOrderService .Repository.QueryFirst(x => x.Id == outboundOrderDetail.OrderId);
                    Dt_OutboundOrder outboundOrder = _outboundOrderService.Repository.QueryFirst(x => x.Id == outboundOrderDetail.OrderId);
                    TaskTypeEnum typeEnum = outboundOrder.OrderType switch
                    {
                        (int)OutOrderTypeEnum.Issue => TaskTypeEnum.Outbound,
@@ -586,5 +631,200 @@
        }
        #region åˆ†æ‰¹åˆ†é…åº“å­˜
        /// <summary>
        /// åˆ†æ‰¹åˆ†é…åº“å­˜
        /// </summary>
        public async Task<WebResponseContent> GenerateOutboundBatchTasksAsync(int orderDetailId, decimal batchQuantity, string outStation)
        {
            try
            {
                List<Dt_Task> tasks = new List<Dt_Task>();
                List<Dt_StockInfo> stockInfos = new List<Dt_StockInfo>();
                List<Dt_OutboundOrderDetail> outboundOrderDetails = new List<Dt_OutboundOrderDetail>();
                List<Dt_OutStockLockInfo> outStockLockInfos = new List<Dt_OutStockLockInfo>();
                List<Dt_LocationInfo> locationInfos = new List<Dt_LocationInfo>();
                (List<Dt_Task>, List<Dt_StockInfo>?, List<Dt_OutboundOrderDetail>?, List<Dt_OutStockLockInfo>?, List<Dt_LocationInfo>?) result = await BatchAllocateStockDataHandle(orderDetailId, batchQuantity, outStation);
                if (result.Item2 != null && result.Item2.Count > 0)
                {
                    stockInfos.AddRange(result.Item2);
                }
                if (result.Item3 != null && result.Item3.Count > 0)
                {
                    outboundOrderDetails.AddRange(result.Item3);
                }
                if (result.Item4 != null && result.Item4.Count > 0)
                {
                    outStockLockInfos.AddRange(result.Item4);
                }
                if (result.Item5 != null && result.Item5.Count > 0)
                {
                    locationInfos.AddRange(result.Item5);
                }
                if (result.Item1 != null && result.Item1.Count > 0)
                {
                    tasks.AddRange(result.Item1);
                }
                WebResponseContent content = await GenerateOutboundTaskDataUpdateAsync(tasks, stockInfos, outboundOrderDetails, outStockLockInfos, locationInfos);
                return content;
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                _logger.LogError($"分批分配库存失败 -  OrderDetailId: {orderDetailId}, Quantity: {batchQuantity}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error($"分批分配失败:{ex.Message}");
            }
        }
        /// <summary>
        /// åˆ†æ‰¹åˆ†é…åº“存数据处理
        /// </summary>
        public async Task<(List<Dt_Task>, List<Dt_StockInfo>?, List<Dt_OutboundOrderDetail>?, List<Dt_OutStockLockInfo>?, List<Dt_LocationInfo>?)>
            BatchAllocateStockDataHandle( int orderDetailId, decimal batchQuantity, string outStation)
        {
            List<Dt_Task> tasks = new List<Dt_Task>();
            // èŽ·å–è®¢å•æ˜Žç»†
            var outboundOrderDetail = await _outboundOrderDetailService.Db.Queryable<Dt_OutboundOrderDetail>()
                .FirstAsync(x => x.Id == orderDetailId );
            if (outboundOrderDetail == null)
            {
                throw new Exception("未找到出库单明细信息");
            }
            var  outboundOrder = await _outboundOrderService.Db.Queryable<Dt_OutboundOrder>().FirstAsync(x => x.Id == outboundOrderDetail.OrderId);
            if(outboundOrder == null)
            {
                throw new Exception("未找到出库单信息");
            }
            // éªŒè¯è®¢å•明细状态
            if (outboundOrderDetail.OrderDetailStatus > OrderDetailStatusEnum.New.ObjToInt() &&
                outboundOrderDetail.OrderDetailStatus != OrderDetailStatusEnum.AssignOverPartial.ObjToInt())
            {
                throw new Exception("所选出库单明细存在出库中或已完成");
            }
            // éªŒè¯åˆ†é…æ•°é‡
            decimal allocatedQty = outboundOrderDetail.AllocatedQuantity;
            decimal overOutQty = outboundOrderDetail.OverOutQuantity;
            decimal needOutQty = outboundOrderDetail.NeedOutQuantity;
            decimal availableQty = needOutQty - allocatedQty - overOutQty;
            if (availableQty <= 0)
                throw new Exception("无可分配数量");
            if (batchQuantity > availableQty)
                throw new Exception($"分配数量不能超过可分配数量{availableQty}");
            List<Dt_StockInfo>? stockInfos = null;
            List<Dt_OutboundOrderDetail>? orderDetails = null;
            List<Dt_OutStockLockInfo>? outStockLockInfos = null;
            List<Dt_LocationInfo>? locationInfos = null;
            // ç”Ÿæˆæ‰¹æ¬¡å·
            string batchNo = await GenerateBatchNo();
            // åˆ†é…åº“å­˜
            (List<Dt_StockInfo>, List<Dt_OutboundOrderDetail>, List<Dt_OutStockLockInfo>, List<Dt_LocationInfo>) allocateResult =
                await _outboundOrderDetailService.AssignStockForBatch(outboundOrderDetail, batchQuantity, batchNo);
            if (allocateResult.Item1 != null && allocateResult.Item1.Count > 0)
            {
                // åˆ›å»ºåˆ†æ‰¹è®°å½•
                await CreateBatchRecord(outboundOrder.OrderNo, orderDetailId, batchQuantity, batchNo);
                TaskTypeEnum typeEnum = outboundOrder.OrderType switch
                {
                    (int)OutOrderTypeEnum.Issue => TaskTypeEnum.Outbound,
                    (int)OutOrderTypeEnum.Allocate => TaskTypeEnum.OutAllocate,
                    (int)OutOrderTypeEnum.Quality => TaskTypeEnum.OutQuality,
                    _ => TaskTypeEnum.Outbound
                };
                tasks = GetTasks(allocateResult.Item1, typeEnum, outStation);
                tasks.ForEach(x =>
                {
                    x.OrderNo = outboundOrder.OrderNo;
                });
                allocateResult.Item2.ForEach(x =>
                {
                    x.OrderDetailStatus = OrderDetailStatusEnum.Outbound.ObjToInt();
                });
                allocateResult.Item3.ForEach(x =>
                {
                    x.Status = OutLockStockStatusEnum.出库中.ObjToInt();
                });
                stockInfos = allocateResult.Item1;
                orderDetails = allocateResult.Item2;
                outStockLockInfos = allocateResult.Item3;
                locationInfos = allocateResult.Item4;
            }
            else
            {
                throw new Exception("无库存");
            }
            return (tasks, stockInfos, orderDetails, outStockLockInfos, locationInfos);
        }
        /// <summary>
        /// æ›´æ–°è®¢å•明细状态
        /// </summary>
        private void UpdateOrderDetailStatus(List<Dt_OutboundOrderDetail> details, decimal allocatedQuantity, decimal needQuantity)
        {
            foreach (var detail in details)
            {
                // æ ¹æ®åˆ†é…æƒ…况更新状态
                if (allocatedQuantity >= needQuantity)
                {
                    detail.OrderDetailStatus = OrderDetailStatusEnum.Outbound.ObjToInt();
                }
                else
                {
                    detail.OrderDetailStatus = OrderDetailStatusEnum.AssignOverPartial.ObjToInt();
                }
            }
        }
        private async Task<string> GenerateBatchNo()
        {
            var batchNo = UniqueValueGenerator.Generate();
            return $"Out{batchNo} ";
        }
        private async Task<Dt_OutboundBatch> CreateBatchRecord(string orderNo, int orderDetailId, decimal batchQuantity, string batchNo)
        {
            var batchRecord = new Dt_OutboundBatch
            {
                BatchNo = batchNo,
                OrderNo = orderNo,
                OrderDetailId = orderDetailId,
                BatchQuantity = batchQuantity,
                BatchStatus = (int)BatchStatusEnum.分配中,
                Operator = App.User.UserName
            };
            await _OutboundBatchRepository.Db.Insertable(batchRecord).ExecuteCommandAsync();
            return batchRecord;
        }
        #endregion
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WIDESEA_TaskInfoService.csproj
@@ -7,6 +7,7 @@
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\WIDESEA_BasicService\WIDESEA_BasicService.csproj" />
    <ProjectReference Include="..\WIDESEA_IAllocateService\WIDESEA_IAllocateService.csproj" />
    <ProjectReference Include="..\WIDESEA_IBasicService\WIDESEA_IBasicService.csproj" />
    <ProjectReference Include="..\WIDESEA_IInboundService\WIDESEA_IInboundService.csproj" />
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Allocate/AllocateOrderController.cs
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System.Data.Common;
using System.Diagnostics.Eventing.Reader;
using System.Threading.Tasks;
@@ -33,6 +34,7 @@
        [HttpPost, Route("ReceiveAllocateOrder"), MethodParamsValidate, AllowAnonymous]
        public async Task<WebResponseContent> ReceiveAllocateOrder([FromBody] AllocateDto model)
        {
            _logger.LogInformation("AllocateOrderController ReceiveAllocateOrder:  " + JsonConvert.SerializeObject(model));
            Dt_AllocateOrder allocateOrder = new Dt_AllocateOrder
            {
                OrderNo = model.OrderNo,
@@ -41,12 +43,20 @@
                FactoryArea = model.FactoryArea,
                IsBatch = model.IsBatch,
                CreateType = model.OperationType,
                FromWarehouse=model.fromWarehouse,
                ToWarehouse=model.toWarehouse,
                FromWarehouse = model.fromWarehouse,
                ToWarehouse = model.toWarehouse,
                Details = new List<Dt_AllocateOrderDetail>()
            };
            Enum.TryParse<BusinessTypeEnum>(allocateOrder.BusinessType, out var businessType);
            //allocateOrder.OrderType = businessType == BusinessTypeEnum.智仓调外部仓库 ? 215 : 115;
            if (businessType == BusinessTypeEnum.智仓调外部仓库)
            {
                allocateOrder.OrderType = 215;
            }
            else if (businessType == BusinessTypeEnum.外部仓库调智仓)
            {
                allocateOrder.OrderType = 115;
            }
            foreach (var detailDto in model.Details)
            {
@@ -69,7 +79,9 @@
                            Barcode = barcodeDto.Barcode,
                            BatchNo = barcodeDto.BatchNo,
                            BarcodeQty = barcodeDto.Qty,
                            BarcodeUnit = barcodeDto.Unit
                            BarcodeUnit = barcodeDto.Unit,
                            ValidDate = barcodeDto.validDate,
                        };
                        allocateOrder.Details.Add(orderDetail);
                    }
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Check/ReCheckOrderController.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Mvc;
using WIDESEA_Core.BaseController;
using WIDESEA_ICheckService;
using WIDESEA_Model.Models.Check;
namespace WIDESEA_WMSServer.Controllers.Check
{
    /// <summary>
    /// é‡æ£€å•
    /// </summary>
    [Route("api/ReCheckOrder")]
    [ApiController]
    public class ReCheckOrderController : ApiBaseController<IReCheckOrderService, Dt_ReCheckOrder>
    {
        public ReCheckOrderController(IReCheckOrderService service) : base(service)
        {
        }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/ESSController.cs
@@ -46,8 +46,9 @@
            _logger.LogInformation(" ESSController  ContainerArrivalReport : CallId={CallId},ContainerCode={ContainerCode},SlotCode={SlotCode}", request.CallId, request.ContainerCode, request.SlotCode);
            var response = new ApiResponse<ContainerArrivalResponseData>
            {
                Code = 1
                Code = 1,
                Data = null,
            };
            // ç”Ÿæˆè¯·æ±‚的唯一标识(基于callId + æ—¶é—´æˆ³ï¼‰
@@ -77,6 +78,11 @@
                }
                WebResponseContent result = await _taskService.RequestInboundTask(request.ContainerCode, request.SlotCode);
                if (result != null && !string.IsNullOrEmpty(result.Message))
                {
                    _logger.LogError(" ESSController  ContainerArrivalReport  RequestInboundTask: Message={Message}", result?.Message);
                }
                var cacheOptions = new MemoryCacheEntryOptions
                {
@@ -87,17 +93,23 @@
                {
                    Code = 0,
                    Msg = "",
                    Data = new ContainerArrivalResponseData
                    {
                        direction = "100"
                    }
                    Data = null,
                };
                if (result != null && !string.IsNullOrEmpty( result.Message ) && result.Message.Contains("该托盘已生成任务"))
                {
                    response.Data = new ContainerArrivalResponseData
                    {
                        direction = "100"
                    };
                    return Ok(response);
                }
                if (result != null && result.Status)
                {
                {
                    response.Data = new ContainerArrivalResponseData
                    {
                        direction = "100"
                    };
                    return Ok(response);
                }
                else
@@ -217,7 +229,7 @@
            _logger.LogInformation("任务完成: TaskCode={TaskCode}, Container={Container}, Robot={Robot}",
                request.TaskCode, request.ContainerCode, request.RobotCode);
            _taskService.TaskCompleted(request.TaskCode);
            await _taskService.TaskCompleted(request.TaskCode);
            // æ ¹æ®ä¸åŒçš„任务类型进行特殊处理
            if (request.Weight.HasValue)
            {
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Inbound/InboundOrderController.cs
@@ -30,13 +30,13 @@
        private readonly WIDESEA_IBasicService.IErpApiService erpApiService;
        private readonly WIDESEA_IBasicService.IInvokeMESService _invokeMESService;
        public readonly IInboundService _inboundService;
        private readonly IESSApiService _eSSApiService;
        private readonly ILocationInfoService _locationInfoService;
        private readonly IDailySequenceService _dailySequenceService;
        private readonly IMaterialUnitService _materialUnitService;
        private readonly ILogger<InboundOrderController> _logger;
        public InboundOrderController(IInboundOrderService service, WIDESEA_IBasicService.IErpApiService erpApiService, WIDESEA_IBasicService.IInvokeMESService invokeMESService, IESSApiService eSSApiService, IDailySequenceService dailySequenceService, ILocationInfoService locationInfoService, ILogger<InboundOrderController> logger, IMaterialUnitService materialUnitService) : base(service)
        public InboundOrderController(IInboundOrderService service, WIDESEA_IBasicService.IErpApiService erpApiService, WIDESEA_IBasicService.IInvokeMESService invokeMESService, IESSApiService eSSApiService, IDailySequenceService dailySequenceService, ILocationInfoService locationInfoService, ILogger<InboundOrderController> logger, IMaterialUnitService materialUnitService, IInboundService inboundService) : base(service)
        {
            this.erpApiService = erpApiService;
            _invokeMESService = invokeMESService;
@@ -45,17 +45,23 @@
            _locationInfoService = locationInfoService;
            _logger = logger;
            _materialUnitService = materialUnitService;
            _inboundService = inboundService;
        }
        [HttpPost, Route("Test"), AllowAnonymous, MethodParamsValidate]
        public async Task<WebResponseContent> Test()
        {
           // var purchaseToStockResult = await _materialUnitService.ConvertPurchaseToStockAsync("101001-00002", 10);
           // var pdddurchaseToStockResult = await _materialUnitService.ConvertPurchaseToStockAsync("100513-00210", 10);
            // Service.Db.Deleteable<Dt_InboundOrder>().Where(x=>x.UpperOrderNo== "12020251100040").ExecuteCommand();
            //_inboundService.InboundOrderDetailService.Db.Deleteable<Dt_InboundOrderDetail>()
            // .Where(p => SqlFunc.Subqueryable<Dt_InboundOrder>().Where(s => s.Id == p.OrderId && s.UpperOrderNo == "12020251100040").Any()).ExecuteCommand();
            var sddd = _locationInfoService.AssignLocation();
            var code = sddd.LocationCode;
            var purchaseToStockResult = await _materialUnitService.ConvertPurchaseToStockAsync("100513-00303", 1);
            var pdddurchaseToStockResult = await _materialUnitService.ConvertFromToStockAsync("100513-00303", "W013", 1);
            //var sddd = _locationInfoService.AssignLocation();
            //var code = sddd.LocationCode;
            //var ssss=await _dailySequenceService.GetNextSequenceAsync();
            //var  ddddssss = "WSLOT" + DateTime.Now.ToString("yyyyMMddHHmmss") + ssss.ToString().PadLeft(5, '0');
            //erpApiService.GetSuppliersAsync();
@@ -88,7 +94,7 @@
            //await  erpApiService.GetMaterialInfoAsync(new WIDESEA_DTO.Basic.MaterialRequest());
            return WebResponseContent.Instance.OK(code);
            return WebResponseContent.Instance.OK();
        }
        /// <summary>
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Outbound/OutboundBatchPickingController.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,219 @@
using Microsoft.AspNetCore.Mvc;
using WIDESEA_Core;
using WIDESEA_Core.BaseController;
using WIDESEA_DTO.Outbound;
using WIDESEA_IOutboundService;
using WIDESEA_Model.Models;
using static WIDESEA_OutboundService.OutboundBatchPickingService;
namespace WIDESEA_WMSServer.Controllers.Outbound
{
    [Route("api/OutboundBatchPicking")]
    [ApiController]
    public class OutboundBatchPickingController : ApiBaseController<IOutboundBatchPickingService, Dt_PickingRecord>
    {
        private readonly ISplitPackageService _splitPackageService;
        private readonly IOutStockLockInfoService _outStockLockInfoService;
        private readonly IOutboundBatchPickingService _outboundBatchPickingService;
        private readonly ILogger<OutboundBatchPickingController> _logger;
        public OutboundBatchPickingController(IOutboundBatchPickingService service, ISplitPackageService splitPackageService, IOutStockLockInfoService outStockLockInfoService, IOutboundBatchPickingService outboundBatchPickingService, ILogger<OutboundBatchPickingController> logger) : base(service)
        {
            _splitPackageService = splitPackageService;
            _outStockLockInfoService = outStockLockInfoService;
            _outboundBatchPickingService = outboundBatchPickingService;
            _logger = logger;
        }
        /// <summary>
        /// åˆ†æ‹£ç¡®è®¤
        /// </summary>
        [HttpPost("confirm-picking")]
        public async Task<WebResponseContent> ConfirmPicking([FromBody] ConfirmPickingDto dto)
        {
            return await _outboundBatchPickingService.ConfirmBatchPicking(dto.OrderNo, dto.PalletCode, dto.Barcode);
        }
        /// <summary>
        /// å–消分拣
        /// </summary>
        [HttpPost("cancel-picking")]
        public async Task<WebResponseContent> CancelPicking([FromBody] CancelPickingDto dto)
        {
            return await _outboundBatchPickingService.CancelPicking(dto.OrderNo, dto.PalletCode, dto.Barcode);
        }
        /// <summary>
        /// å–消拆包链
        /// </summary>
        [HttpPost("cancel-split-chain")]
        public async Task<WebResponseContent> CancelSplitChain([FromBody] CancelSplitChainDto dto)
        {
            return await _outboundBatchPickingService.CancelSplitPackageChain(dto.OrderNo, dto.PalletCode, dto.StartBarcode);
        }
        /// <summary>
        /// èŽ·å–æ‹†åŒ…é“¾ä¿¡æ¯
        /// </summary>
        [HttpPost("split-package-chain-info")]
        public async Task<WebResponseContent> GetSplitPackageChainInfo([FromBody] SplitPackageChainInfoRequestDto dto)
        {
            return await _outboundBatchPickingService.GetSplitPackageChainInfo(dto.OrderNo, dto.Barcode);
        }
        /// <summary>
        /// æŸ¥æ‰¾å®Œæ•´æ‹†åŒ…链(从根条码开始)
        /// </summary>
        [HttpPost("find-root-split-chain")]
        public async Task<WebResponseContent> FindRootSplitChain([FromBody] SplitPackageChainInfoRequestDto dto)
        {
            try
            {
                // æŸ¥æ‰¾æ ¹æ¡ç 
                var rootBarcode = await _outboundBatchPickingService. FindRootBarcode(dto.OrderNo, dto.Barcode);
                // èŽ·å–å®Œæ•´æ‹†åŒ…é“¾
                var splitChain = await _outboundBatchPickingService.GetSplitPackageChain(dto.OrderNo, rootBarcode);
                var chainInfo = new SplitPackageChainInfoDto
                {
                    OriginalBarcode = rootBarcode,
                    RootBarcode = rootBarcode,
                    TotalSplitTimes = splitChain.Count,
                    ChainType = "root",
                    SplitChain = splitChain.Select(x => new SplitChainItemDto
                    {
                        SplitTime = x.SplitTime,
                        OriginalBarcode = x.OriginalBarcode,
                        NewBarcode = x.NewBarcode,
                        SplitQuantity = x.SplitQty,
                        Operator = x.Operator,
                        IsReverted = x.IsReverted
                    }).ToList()
                };
                return WebResponseContent.Instance.OK("获取成功", chainInfo);
            }
            catch (Exception ex)
            {
                _logger.LogError($"查找完整拆包链失败 - OrderNo: {dto.OrderNo}, Barcode: {dto.Barcode}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error("查找完整拆包链失败");
            }
        }
        /// <summary>
        /// æ‰‹åŠ¨æ‹†åŒ…
        /// </summary>
        [HttpPost("split-package")]
        public async Task<WebResponseContent> SplitPackage([FromBody] SplitPackageDto dto)
        {
            return await _outboundBatchPickingService.ManualSplitPackage(dto.OrderNo, dto.PalletCode, dto.OriginalBarcode, dto.SplitQuantity);
        }
        /// <summary>
        /// å–消拆包
        /// </summary>
        [HttpPost("cancel-split")]
        public async Task<WebResponseContent> CancelSplit([FromBody] CancelSplitDto dto)
        {
            return await _outboundBatchPickingService.CancelSplitPackage(dto.OrderNo, dto.PalletCode, dto.NewBarcode);
        }
        /// <summary>
        /// åˆ†æ‰¹å›žåº“
        /// </summary>
        [HttpPost("return-stock")]
        public async Task<WebResponseContent> ReturnStock([FromBody] ReturnStockDto dto)
        {
            return await _outboundBatchPickingService.BatchReturnStock(dto.OrderNo, dto.PalletCode);
        }
        /// <summary>
        /// èŽ·å–æ‰˜ç›˜çš„é”å®šä¿¡æ¯
        /// </summary>
        [HttpPost("pallet-locks")]
        public async Task<WebResponseContent> GetPalletLocks([FromBody] PalletLocksDto dto)
        {
            try
            {
                var locks = await _outboundBatchPickingService.GetPalletLockInfos(dto.OrderNo, dto.PalletCode);
                return WebResponseContent.Instance.OK("获取成功", locks);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, $"获取托盘锁定信息异常 - OrderNo: {dto.OrderNo}, PalletCode: {dto.PalletCode}");
                return WebResponseContent.Instance.Error("系统异常,请稍后重试" + ex.Message);
            }
        }
        /// <summary>
        /// èŽ·å–å·²æ‹£é€‰åˆ—è¡¨
        /// </summary>
        [HttpPost("pallet-picked-list")]
        public async Task<WebResponseContent> GetPalletPickedList([FromBody] PalletLocksDto dto)
        {
            try
            {
                var pickedList = await _outboundBatchPickingService.GetPalletPickedList(dto.OrderNo, dto.PalletCode);
                return WebResponseContent.Instance.OK("获取成功", pickedList);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, $"获取已拣选列表异常 - OrderNo: {dto.OrderNo}, PalletCode: {dto.PalletCode}");
                return WebResponseContent.Instance.Error("系统异常,请稍后重试" + ex.Message);
            }
        }
        /// <summary>
        /// èŽ·å–æ‰˜ç›˜çŠ¶æ€
        /// </summary>
        [HttpPost("pallet-status")]
        public async Task<WebResponseContent> GetPalletStatus([FromBody] PalletLocksDto dto)
        {
            try
            {
                var status = await _outboundBatchPickingService.GetPalletStatus(dto.OrderNo, dto.PalletCode);
                return WebResponseContent.Instance.OK("获取成功", status);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, $"获取托盘状态异常 - OrderNo: {dto.OrderNo}, PalletCode: {dto.PalletCode}");
                return WebResponseContent.Instance.Error("系统异常,请稍后重试" + ex.Message);
            }
        }
        /// <summary>
        /// èŽ·å–æ‹†åŒ…ä¿¡æ¯
        /// </summary>
        [HttpPost("split-package-info")]
        public async Task<WebResponseContent> GetSplitPackageInfo([FromBody] SplitPackageInfoDto dto)
        {
            try
            {
                var info = await _outboundBatchPickingService.GetSplitPackageInfo(dto.OrderNo, dto.PalletCode, dto.Barcode);
                return WebResponseContent.Instance.OK("获取成功", info);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, $"获取拆包信息异常 - OrderNo: {dto.OrderNo}, PalletCode: {dto.PalletCode}, Barcode: {dto.Barcode}");
                return WebResponseContent.Instance.Error("系统异常,请稍后重试" +ex.Message);
            }
        }
        /// <summary>
        /// å–走空箱
        /// </summary>
        [HttpPost("remove-empty-pallet")]
        public async Task<WebResponseContent> RemoveEmptyPallet([FromBody] RemoveEmptyPalletDto dto)
        {
            try
            {
                var result = await _outboundBatchPickingService.RemoveEmptyPallet(dto.OrderNo, dto.PalletCode);
                return result;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, $"取走空箱异常 - OrderNo: {dto.OrderNo}, PalletCode: {dto.PalletCode}");
                return WebResponseContent.Instance.Error("系统异常,请稍后重试" +ex.Message);
            }
        }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Outbound/OutboundPickingController.cs
@@ -92,13 +92,20 @@
            return await Service.ReturnRemaining(dto.OrderNo, dto.PalletCode, "");
        }
        [HttpPost("remove-empty-pallet")]
        public async Task<WebResponseContent> RemoveEmptyPallet ([FromBody] ConfirmPickingDto dto)
        {
            return await Service.RemoveEmptyPallet(dto.OrderNo, dto.PalletCode);
        }
        //[HttpPost("direct-outbound")]
        //public async Task<WebResponseContent> DirectOutbound([FromBody] DirectOutboundRequest dto)
        //{
        //   return await Service.DirectOutbound(dto);
        //} 
        /// <summary>
        /// æ’¤é”€æ‹£é€‰
        /// </summary>
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Stock/StockInfoController.cs
@@ -25,7 +25,7 @@
        /// <param name="orderId"></param>
        /// <param name="materielCode"></param>
        /// <returns></returns>
        [HttpPost, HttpGet, Route("GetStockSelectViews")]
        [HttpPost, HttpGet, Route("GetStockSelectViews"),AllowAnonymous]
        public List<StockSelectViewDTO> GetStockSelectViews(int orderId, string materielCode)
        {
            return Service.GetStockSelectViews(orderId, materielCode);
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/TaskInfo/TaskController.cs
@@ -1,8 +1,11 @@
using Microsoft.AspNetCore.Authorization;
using MailKit.Search;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using System.Threading.Tasks;
using WIDESEA_Common.CommonEnum;
using WIDESEA_Common.TaskEnum;
using WIDESEA_Core;
using WIDESEA_Core.Attributes;
using WIDESEA_Core.BaseController;
@@ -24,12 +27,38 @@
        {
        }
        [HttpPost, Route("AddTestTask"), AllowAnonymous, MethodParamsValidate]
        public WebResponseContent AddTest(WMSTaskDTO wMSTaskDTO)
        {
            Dt_Task task = new Dt_Task
            {
                PalletCode = wMSTaskDTO.PalletCode,
                PalletType = wMSTaskDTO.PalletType,
                Roadway = "t",
                TaskType = wMSTaskDTO.TaskType,
                TaskStatus = wMSTaskDTO.TaskStatus,
                SourceAddress = wMSTaskDTO.SourceAddress,
                TargetAddress = wMSTaskDTO.TargetAddress,
                CurrentAddress = "t",
                NextAddress = "t",
                WarehouseId = wMSTaskDTO.WarehouseId,
                OrderNo = "testt",
                Grade = wMSTaskDTO.Grade,
                Dispatchertime = DateTime.Now,
        [HttpPost, Route("PalletOutboundTask"), AllowAnonymous, MethodParamsValidate]
            };
            Service.AddData(task);
            return WebResponseContent.Instance.OK();
        }
        [HttpPost, Route("PalletOutboundTask"), AllowAnonymous, MethodParamsValidate]
        public async Task<WebResponseContent> PalletOutboundTask(string endStation, string palletCode = "")
        {
            var result = await Service.PalletOutboundTask(endStation, palletCode);
            return result;
@@ -44,7 +73,7 @@
        [HttpPost, HttpGet, Route("GenerateOutboundTasks"), AllowAnonymous]
        public async Task<WebResponseContent> GenerateOutboundTasks([FromBody] GenerateOutboundTasksDto data)
        {
            return await Service.GenerateOutboundTasksAsync(data.taskIds,data.outboundPlatform);
            return await Service.GenerateOutboundTasksAsync(data.taskIds, data.outboundPlatform);
        }
        /// <summary>
@@ -59,5 +88,17 @@
            return Service.GenerateOutboundTask(orderDetailId, stockSelectViews);
        }
        /// <summary>
        /// åˆ†æ‰¹ç”Ÿæˆå‡ºåº“任务
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        [HttpPost, HttpGet, Route("GenerateOutboundBatchTasks"), AllowAnonymous]
        public async Task<WebResponseContent> GenerateOutboundBatchTasks([FromBody] GenerateOutboundBatchTasksDto data)
        {
            return await Service.GenerateOutboundBatchTasksAsync(data.orderDetailId,data.batchQuantity, data.outboundPlatform);
        }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_WMSServer/Jobs/InventoryLockJob.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
using Quartz;
using SqlSugar;
namespace WIDESEA_WMSServer.Jobs
{
    [DisallowConcurrentExecution]
    public class InventoryLockJob : IJob
    {
        private readonly ILogger<ErpJob> _logger;
        private readonly ISqlSugarClient _db;
        public InventoryLockJob(ILogger<ErpJob> logger, ISqlSugarClient db )
        {
            _logger = logger;
            _db = db;
        }
        public Task Execute(IJobExecutionContext context)
        {
           return Task.CompletedTask;
        }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs
@@ -161,6 +161,15 @@
        .ForJob(jobKey)
        .WithIdentity("ErpJob-trigger")
        .WithCronSchedule("0 0 10,14,20 * * ?"));
    var inventoryLockJobKey = new JobKey("InventoryLockJob");
    q.AddJob<InventoryLockJob>(opts => opts.WithIdentity(inventoryLockJobKey));
    q.AddTrigger(opts => opts
        .ForJob(inventoryLockJobKey)
        .WithIdentity("InventoryLockJob-trigger")
        .WithCronSchedule("0 0/10 * * * ?")); // æ¯10分钟执行一次
});