z8018
2025-12-15 741eee21e85ec0cbc8ee1bfc2458c1dd6883114e
出库部分
已添加17个文件
已删除1个文件
已修改8个文件
34912 ■■■■ 文件已修改
项目代码/WIDESEA_WMSClient/.claude/settings.local.json 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/.eslintrc.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/README.md 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/package-lock.json 18036 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/pnpm-lock.yaml 13293 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/extension/outbound/extend/outOrderDetail copy.vue 821 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/extension/outbound/extend/outOrderDetail.vue 124 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/router/viewGird.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/outbound/outPicking.vue 798 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/17.14.1204.46620/CodeChunks.db 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/17.14.1204.46620/SemanticSymbols.db 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_DTO/CalcOut/MaterielOutboundCalculationDTO.cs 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_DTO/CalcOut/OutboundCalculationDTO.cs 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_DTO/CalcOut/OutboundCompleteRequestDTO.cs 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_DTO/CalcOut/OutboundCompleteResponseDTO.cs 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_DTO/CalcOut/PickedListItemDTO.cs 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_DTO/CalcOut/PickedStockDetailDTO.cs 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_DTO/CalcOut/PickingOutboundRequestDTO.cs 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_DTO/CalcOut/PickingOutboundResponseDTO.cs 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_DTO/CalcOut/ScannedStockDetailDTO.cs 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_IOutboundService/IOutboundService.cs 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Model/Models/Outbound/Dt_OutboundLockInfo.cs 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Model/Models/Record/Dt_StockQuantityChangeRecord.cs 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundQueryService.cs 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundService.cs 1140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Outbound/OutboundController.cs 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ÏîÄ¿´úÂë/WIDESEA_WMSClient/.claude/settings.local.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
{
  "permissions": {
    "allow": [
      "Bash(npm run serve:*)",
      "Bash(npm install:*)",
      "Bash(SET NODE_OPTIONS=--openssl-legacy-provider)",
      "Bash(export NODE_OPTIONS=--openssl-legacy-provider:*)",
      "Bash(node:*)",
      "Bash(npm --version:*)"
    ]
  }
}
ÏîÄ¿´úÂë/WIDESEA_WMSClient/.eslintrc.js
ÎļþÒÑɾ³ý
ÏîÄ¿´úÂë/WIDESEA_WMSClient/README.md
@@ -39,3 +39,4 @@
    "lint": "vue-cli-service lint"
}
SET NODE_OPTIONS=--openssl-legacy-provider && npm run serve
ÏîÄ¿´úÂë/WIDESEA_WMSClient/package-lock.json
ÎļþÌ«´ó
ÏîÄ¿´úÂë/WIDESEA_WMSClient/pnpm-lock.yaml
¶Ô±ÈÐÂÎļþ
ÎļþÌ«´ó
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/extension/outbound/extend/outOrderDetail copy.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,821 @@
<template>
  <div>
    <vol-box
      v-model="showDetialBox"
      :lazy="true"
      width="75%"
      :padding="15"
      title="单据明细信息"
    >
      <div class="box-head">
        <el-alert :closable="false" style="width: 100%">
          <el-row>
            <el-col :span="16">
              <span>已选中 {{ selection.length }} é¡¹</span>
            </el-col>
            <el-col :span="8">
              <el-link
                type="primary"
                size="small"
                v-if="isBatch === 0"
                style="float: right; height: 20px"
                @click="handleOpenPicking"
                >拣选</el-link
              >
              <el-link
                type="primary"
                size="small"
                style="float: right; height: 20px; margin-right: 10px"
                v-if="isBatch === 1"
                @click="handleOpenBatchPicking"
                >分批拣选</el-link
              >
              <el-link
                type="primary"
                size="small"
                v-if="isBatch === 0"
                style="float: right; height: 20px; margin-right: 10px"
                @click="outbound"
                >直接出库</el-link
              >
              <el-link
                type="primary"
                size="small"
                v-if="isBatch === 1"
                style="float: right; height: 20px; margin-right: 10px"
                @click="outboundbatch"
                >分批出库</el-link
              >
              <el-link
                type="primary"
                size="small"
                style="float: right; height: 20px; margin-right: 10px"
                @click="getData"
                >刷新</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
          @current-change="handleCurrentChange"
          height="500px"
          @row-click="handleRowClick"
          @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">
              <div v-if="item.type == 'icon'">
                <el-tooltip
                  class="item"
                  effect="dark"
                  :content="item.title"
                  placement="bottom"
                >
                  <el-link
                    type="primary"
                    :disabled="getButtonEnable(item.prop, scoped.row)"
                    @click="tableButtonClick(scoped.row, item)"
                  >
                    <i :class="item.icon" style="font-size: 22px"></i>
                  </el-link>
                </el-tooltip>
              </div>
              <div v-else-if="item.type == 'tag'">
                <el-tag size="small">
                  {{ getDictionary(scoped.row, item) }}
                </el-tag>
              </div>
              <div v-else>
                {{ scoped.row[item.prop] }}
              </div>
            </template>
          </el-table-column>
        </el-table>
      </div>
    </vol-box>
    <stock-select ref="child" @parentCall="parentCall"></stock-select>
    <selected-stock
      ref="selectedStock"
      @parentCall="parentCall"
    ></selected-stock>
    <NoStockOut ref="NoStockOut" @parentCall="parentCall"></NoStockOut>
  </div>
</template>
<script>
import VolBox from "@/components/basic/VolBox.vue";
import VolForm from "@/components/basic/VolForm.vue";
import StockSelect from "./StockSelect.vue";
import SelectedStock from "./SelectedStock.vue";
import NoStockOut from "./NoStockOut.vue";
import { h, createVNode, render, reactive } from "vue";
import {
  ElDialog,
  ElForm,
  ElFormItem,
  ElSelect,
  ElOption,
  ElButton,
  ElInput,
  ElMessage,
} from "element-plus";
export default {
  components: { VolBox, VolForm, StockSelect, SelectedStock, NoStockOut },
  data() {
    return {
      row: null,
      isBatch: 0,
      showDetialBox: false,
      flag: false,
      currentRow: null,
      selection: [],
      tableData: [],
      mainBusinessType: null, // æ–°å¢žï¼šå­˜å‚¨ä¸»å•据的businessType
      tableColumns: [
        {
          prop: "id",
          title: "Id",
          type: "int",
          width: 90,
          hidden: true,
        },
        {
          prop: "orderId",
          title: "出库单主键",
          type: "string",
          width: 90,
          hidden: true,
        },
        {
          prop: "materielCode",
          title: "物料编号",
          type: "string",
          width: 120,
        },
        {
          prop: "materielName",
          title: "物料名称",
          type: "string",
          width: 150,
        },
        {
          prop: "batchNo",
          title: "批次号",
          type: "string",
          width: 90,
        },
        {
          prop: "supplyCode",
          title: "供应商编号",
          type: "string",
          width: 90,
        },
        {
          prop: "orderQuantity",
          title: "单据数量",
          type: "string",
          width: 90,
        },
        {
          prop: "lockQuantity",
          title: "锁定数量",
          type: "int",
          width: 90,
        },
        {
          prop: "overOutQuantity",
          title: "已出数量",
          type: "string",
          width: 90,
        },
        {
          prop: "moveQty",
          title: "挪料数量",
          type: "string",
          width: 90,
        },
        {
          prop: "unit",
          title: "单位",
          type: "string",
          width: 80,
        },
        {
          prop: "orderDetailStatus",
          title: "订单明细状态",
          type: "tag",
          width: 90,
          bindKey: "orderDetailStatusEnum",
        },
        {
          prop: "assignStock",
          title: "指定库存",
          type: "icon",
          width: 90,
          hidden: true, // é»˜è®¤éšè—
          icon: "el-icon-s-grid",
        },
        {
          prop: "viewDetail",
          title: "出库详细",
          type: "icon",
          width: 90,
          icon: "el-icon-s-operation",
        },
        {
          prop: "creater",
          title: "创建人",
          type: "string",
          width: 90,
        },
        {
          prop: "createDate",
          title: "创建时间",
          type: "datetime",
          width: 160,
        },
        {
          prop: "modifier",
          title: "修改人",
          type: "string",
          width: 100,
        },
        {
          prop: "modifyDate",
          title: "修改时间",
          type: "datetime",
          width: 160,
        },
        {
          prop: "remark",
          title: "备注",
          type: "string",
        },
      ],
      paginations: {
        sort: "id",
        order: "desc",
        Foots: "",
        total: 0,
        sizes: [30, 60, 100, 120],
        size: 30,
        Wheres: [],
        page: 1,
        rows: 30,
      },
      searchFormOptions: [
        [
          {
            title: "单据编号",
            field: "allocation_code",
            type: "like",
          },
          {
            title: "单据类型",
            field: "allocation_type",
            type: "select",
            dataKey: "OrderType",
            data: [],
          },
          {
            title: "单据状态",
            field: "allocation_state",
            type: "select",
            dataKey: "OrderState",
            data: [],
          },
        ],
      ],
      searchFormFields: {
        allocation_code: "",
        allocation_type: "",
        allocation_state: "",
      },
      dictionaryList: null,
    };
  },
  methods: {
    toggleAssignStockColumn() {
      const assignStockColumn = this.tableColumns.find(
        (item) => item.prop === "assignStock"
      );
      if (assignStockColumn) {
        // businessType为22时显示,否则隐藏
        assignStockColumn.hidden = this.mainBusinessType !=='22';
      }
    },
    open(row) {
      this.row = row;
      this.showDetialBox = true;
      console.log("主单据数据:", this.row);
      this.isBatch = row.isBatch;
      this.mainBusinessType = row.businessType;
      this.getDictionaryData();
      this.getData();
      this.toggleAssignStockColumn();
    },
    getData() {
      var wheres = [{ name: "orderId", value: this.row.id }];
      var param = {
        page: this.paginations.page,
        rows: this.paginations.rows,
        sort: this.paginations.sort,
        order: this.paginations.order,
        wheres: JSON.stringify(wheres),
      };
      this.http
        .post("api/OutboundOrderDetail/GetPageData", param, "查询中")
        .then((x) => {
          this.tableData = x.rows;
          this.toggleAssignStockColumn(); // æ•°æ®åŠ è½½åŽé‡æ–°ç¡®è®¤åˆ—æ˜¾éš
        });
    },
    tableButtonClick(row, column) {
      if (column.prop == "assignStock") {
        this.$refs.child.open(row);
      } else if (column.prop == "NoStockOut") {
        this.$refs.NoStockOut.open(row);
      } else {
        this.$refs.selectedStock.open(row);
      }
    },
    lockstocks() {
      if (this.selection.length === 0) {
        return this.$message.error("请选择单据明细");
      }
      var keys = this.selection.map((item) => item.id);
      this.http
        .post("api/OutboundOrderDetail/LockOutboundStocks", keys, "数据处理中")
        .then((x) => {
          if (!x.status) return this.$message.error(x.message);
          this.$message.success("操作成功");
          this.showDetialBox = false;
          this.$emit("parentCall", ($vue) => {
            $vue.getData();
          });
        });
    },
    handleOpenPicking() {
      this.$router.push({
        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) {
        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);
      const formData = reactive({
        selectedPlatform: platformOptions[0].value,
      });
      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" },
                  ],
                },
                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("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;
                      }
                      const keys = this.selection.map((item) => item.id);
                      const requestParams = {
                        taskIds: keys,
                        outboundPlatform: formData.selectedPlatform,
                      };
                      this.http
                        .post(
                          "api/Task/GenerateOutboundTasks",
                          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",
                    },
                  }, "确定出库"),
                ]),
              ]),
        }
      );
      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);
      const formData = reactive({
        selectedPlatform: platformOptions[0].value,
        outboundDecimal: "",
      });
      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) => {
                        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",
                    precision: 2,
                    min: 0.01,
                  }),
                ]),
                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;
                      }
                      const keys = this.selection.map((item) => item.id);
                      const requestParams = {
                        orderDetailId: keys[0],
                        outboundPlatform: formData.selectedPlatform,
                        batchQuantity: formData.outboundDecimal,
                      };
                      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",
                    },
                  }, "确定分批出库"),
                ]),
              ]),
        }
      );
      vnode.appContext = this.$.appContext;
      render(vnode, mountNode);
    },
    setCurrent(row) {
      this.$refs.singleTable.setCurrentRow(row);
    },
    handleCurrentChange(val) {
      this.currentRow = val;
    },
    getButtonEnable(propName, row) {
      if (propName == "assignStock") {
        if (
          row.orderDetailStatus !== 0 &&
          row.orderDetailStatus !== 60 &&
          row.orderDetailStatus !== 70 &&
          row.orderDetailStatus !== 80
        ) {
          return true;
        } else {
          return false;
        }
      }
      return false;
    },
    parentCall(fun) {
      if (typeof fun != "function") {
        return console.log("扩展组件需要传入一个回调方法才能获取父级Vue对象");
      }
      fun(this);
    },
    handleRowClick(row) {
      this.$refs.singleTable.toggleRowSelection(row);
    },
    handleSelectionChange(val) {
      this.selection = val;
    },
    getDictionaryData() {
      if (this.dictionaryList) {
        return;
      }
      var param = [];
      this.tableColumns.forEach((x) => {
        if (x.type == "tag" && x.bindKey != "") {
          param.push(x.bindKey);
        }
      });
      this.http
        .post("api/Sys_Dictionary/GetVueDictionary", param, "查询中")
        .then((x) => {
          if (x.length > 0) {
            this.dictionaryList = x;
          }
        });
    },
    getDictionary(row, column) {
      if (this.dictionaryList) {
        var item = this.dictionaryList.find((x) => x.dicNo == column.bindKey);
        if (item) {
          var dicItem = item.data.find((x) => x.key == row[column.prop]);
          if (dicItem) {
            return dicItem.value;
          } else {
            return row[column.prop];
          }
        } else {
          return row[column.prop];
        }
      }
      return row[column.prop];
    },
  },
};
</script>
<style scoped>
.text-button {
  border: 0px;
}
</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;
}
</style>
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/extension/outbound/extend/outOrderDetail.vue
@@ -1,12 +1,6 @@
<template>
  <div>
    <vol-box
      v-model="showDetialBox"
      :lazy="true"
      width="75%"
      :padding="15"
      title="单据明细信息"
    >
    <vol-box v-model="showDetialBox" :lazy="true" width="75%" :padding="15" title="单据明细信息">
      <div class="box-head">
        <el-alert :closable="false" style="width: 100%">
          <el-row>
@@ -14,89 +8,33 @@
              <span>已选中 {{ selection.length }} é¡¹</span>
            </el-col>
            <el-col :span="8">
              <el-link
                type="primary"
                size="small"
                v-if="isBatch === 0"
                style="float: right; height: 20px"
                @click="handleOpenPicking"
                >拣选</el-link
              >
              <el-link
                type="primary"
                size="small"
                style="float: right; height: 20px; margin-right: 10px"
                v-if="isBatch === 1"
                @click="handleOpenBatchPicking"
                >分批拣选</el-link
              >
              <el-link
                type="primary"
                size="small"
                v-if="isBatch === 0"
                style="float: right; height: 20px; margin-right: 10px"
                @click="outbound"
                >直接出库</el-link
              >
              <el-link
                type="primary"
                size="small"
                v-if="isBatch === 1"
                style="float: right; height: 20px; margin-right: 10px"
                @click="outboundbatch"
                >分批出库</el-link
              >
              <el-link
                type="primary"
                size="small"
                style="float: right; height: 20px; margin-right: 10px"
                @click="getData"
                >刷新</el-link
              >
              <el-link type="primary" size="small" v-if="isBatch === 0" style="float: right; height: 20px"
                @click="handleOpenPicking">拣选</el-link>
              <el-link type="primary" size="small" style="float: right; height: 20px; margin-right: 10px"
                v-if="isBatch === 1" @click="handleOpenBatchPicking">分批拣选</el-link>
              <el-link type="primary" size="small" v-if="isBatch === 0"
                style="float: right; height: 20px; margin-right: 10px" @click="outbound">直接出库</el-link>
              <el-link type="primary" size="small" v-if="isBatch === 1"
                style="float: right; height: 20px; margin-right: 10px" @click="outboundbatch">分批出库</el-link>
              <el-link type="primary" size="small" style="float: right; height: 20px; margin-right: 10px"
                @click="getData">刷新</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
          @current-change="handleCurrentChange"
          height="500px"
          @row-click="handleRowClick"
          @selection-change="handleSelectionChange"
        >
        <el-table ref="singleTable" :data="tableData" style="width: 100%; height: 100%" highlight-current-row
          @current-change="handleCurrentChange" height="500px" @row-click="handleRowClick"
          @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"
          >
          <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">
              <div v-if="item.type == 'icon'">
                <el-tooltip
                  class="item"
                  effect="dark"
                  :content="item.title"
                  placement="bottom"
                >
                  <el-link
                    type="primary"
                    :disabled="getButtonEnable(item.prop, scoped.row)"
                    @click="tableButtonClick(scoped.row, item)"
                  >
                <el-tooltip class="item" effect="dark" :content="item.title" placement="bottom">
                  <el-link type="primary" :disabled="getButtonEnable(item.prop, scoped.row)"
                    @click="tableButtonClick(scoped.row, item)">
                    <i :class="item.icon" style="font-size: 22px"></i>
                  </el-link>
                </el-tooltip>
@@ -115,10 +53,7 @@
      </div>
    </vol-box>
    <stock-select ref="child" @parentCall="parentCall"></stock-select>
    <selected-stock
      ref="selectedStock"
      @parentCall="parentCall"
    ></selected-stock>
    <selected-stock ref="selectedStock" @parentCall="parentCall"></selected-stock>
    <NoStockOut ref="NoStockOut" @parentCall="parentCall"></NoStockOut>
  </div>
</template>
@@ -379,7 +314,7 @@
    },
    handleOpenPicking() {
      this.$router.push({
        path: "/outbound/picking",
        path: "/outbound/outPicking",
        query: { orderId: this.row.id, orderNo: this.row.orderNo },
      });
    },
@@ -494,13 +429,16 @@
                      const keys = this.selection.map((item) => item.id);
                      const requestParams = {
                        taskIds: keys,
                        outboundPlatform: formData.selectedPlatform,
                        detailIds: keys,
                        outboundTargetLocation: formData.selectedPlatform,
                        outboundQuantity: 1,
                        operator: "",
                        orderNo: this.row.orderNo,
                      };
                      this.http
                        .post(
                          "api/Task/GenerateOutboundTasks",
                          "api/Outbound/ProcessPickingOutbound",
                          requestParams,
                          "数据处理中"
                        )
@@ -514,9 +452,9 @@
                          render(null, mountNode);
                          document.body.removeChild(mountNode);
                        })
                        .catch(() => {
                          ElMessage.error("请求失败,请稍后重试");
                        });
                        // .catch(() => {
                        //   ElMessage.error("请求失败,请稍后重试");
                        // });
                    },
                    style: {
                      borderRadius: "4px",
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/router/viewGird.js
@@ -81,6 +81,12 @@
  meta: { title: '拣选确认', keepAlive: false }
},
  {
  path: '/outbound/outPicking',
  name: 'outPicking',
  component: () => import('@/views/outbound/outPicking.vue'),
  meta: { title: '拣选确认', keepAlive: false }
},
  {
  path: '/outbound/batchpicking',
  name: 'BatchPickingConfirm', 
  component: () => import('@/views/outbound/BatchPickingConfirm.vue'),
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/outbound/outPicking.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,798 @@
<template>
    <div class="picking-container">
        <!-- é¡¶éƒ¨è®¢å•信息 -->
        <el-card class="order-info-card" shadow="never">
            <div class="order-header">
                <div class="order-title">
                    <i class="el-icon-document"></i>
                    <span class="order-label">订单号:</span>
                    <span class="order-value">{{ orderNo }}</span>
                </div>
                <div class="order-status" v-if="orderInfo">
                    <el-tag :type="getStatusType(orderInfo.status)" size="medium">
                        {{ orderInfo.statusName || '进行中' }}
                    </el-tag>
                </div>
            </div>
        </el-card>
        <!-- æ‰«ç æ“ä½œåŒºåŸŸ -->
        <el-card class="scan-section-card" shadow="never">
            <div class="scan-section">
                <el-alert title="请使用扫码枪扫描,支持回车自动确认" type="info" :closable="false" show-icon class="scan-alert">
                    <template #default>
                        <span>1. è¯·å…ˆæ‰«ææ‰˜ç›˜ç  â†’ 2. å†æ‰«æç‰©æ–™æ¡ç </span>
                    </template>
                </el-alert>
                <el-form :model="scanForm" :rules="scanRules" ref="scanFormRef" class="scan-form">
                    <el-row :gutter="20">
                        <el-col :span="8">
                            <el-form-item label="托盘码" prop="palletCode">
                                <el-input ref="palletInput" v-model="scanForm.palletCode" placeholder="请扫描托盘码"
                                    size="large" clearable @keyup.enter="handlePalletScan">
                                    <template #prefix>
                                        <i class="el-icon-box"></i>
                                    </template>
                                </el-input>
                            </el-form-item>
                        </el-col>
                        <el-col :span="8">
                            <el-form-item label="物料条码" prop="materialBarcode">
                                <el-input ref="materialInput" v-model="scanForm.materialBarcode" placeholder="请扫描物料条码"
                                    size="large" clearable :disabled="unpickedData.length <= 0"
                                    @keyup.enter="handleMaterialScan">
                                    <template #prefix>
                                        <i class="el-icon-s-grid"></i>
                                    </template>
                                </el-input>
                            </el-form-item>
                        </el-col>
                        <el-col :span="8">
                            <el-form-item class="button-form-item">
                                <div class="action-buttons">
                                    <el-button type="primary" size="large" @click="handleConfirmPick"
                                        :loading="confirmLoading" :disabled="!canConfirm">
                                        <i class="el-icon-check"></i>
                                        ç¡®è®¤æ‹£é€‰
                                    </el-button>
                                    <el-button type="warning" size="large" @click="handleEmptyBox"
                                        :disabled="!scanForm.palletCode">
                                        <i class="el-icon-delete"></i>
                                        å–空箱
                                    </el-button>
                                    <el-button type="success" size="large" @click="handleReturnToWarehouse"
                                        :disabled="!scanForm.palletCode">
                                        <i class="el-icon-refresh-left"></i>
                                        å›žåº“
                                    </el-button>
                                </div>
                            </el-form-item>
                        </el-col>
                    </el-row>
                </el-form>
            </div>
        </el-card>
        <!-- æ•°æ®åˆ—表区域 -->
        <div class="tables-section">
            <el-row :gutter="20">
                <!-- æœªæ‹£é€‰åˆ—表 -->
                <el-col :span="12">
                    <el-card class="table-card" shadow="never">
                        <template #header>
                            <div class="card-header">
                                <span class="card-title">
                                    <i class="el-icon-time"></i>
                                    æœªæ‹£é€‰åˆ—表
                                </span>
                                <el-badge :value="unpickedCount" class="badge-item" type="warning">
                                    <el-button size="small" @click="refreshUnpickedTable" icon="el-icon-refresh">
                                        åˆ·æ–°
                                    </el-button>
                                </el-badge>
                            </div>
                        </template>
                        <el-table ref="unpickedTable" :data="unpickedData" height="400" stripe highlight-current-row>
                            <el-table-column type="index" label="序号" width="60" align="center" />
                            <el-table-column prop="materielCode" label="物料编码" width="120" show-overflow-tooltip />
                            <el-table-column prop="materielName" label="物料名称" width="80" show-overflow-tooltip />
                            <el-table-column prop="batchNo" label="批次号" width="100" />
                            <el-table-column prop="assignQuantity" label="分拣数量" width="80" align="right">
                                <template #default="scope">
                                    <el-text type="danger">{{ scope.row.assignQuantity }}</el-text>
                                </template>
                            </el-table-column>
                            <el-table-column prop="sortedQuantity" label="已分拣数量" width="120" align="right">
                                <template #default="scope">
                                    <el-text type="danger">{{ scope.row.sortedQuantity }}</el-text>
                                </template>
                            </el-table-column>
                            <el-table-column prop="unit" label="单位" width="60" />
                            <el-table-column prop="locationCode" label="库位" />
                            <!-- <el-table-column label="操作" width="80" align="center">
                                <template #default="scope">
                                    <el-button type="text" size="small" @click="quickPick(scope.row)"
                                        :disabled="!scanForm.palletCode">
                                        æ‹£é€‰
                                    </el-button>
                                </template>
                            </el-table-column> -->
                        </el-table>
                        <div class="table-footer">
                            <el-descriptions :column="2" size="small">
                                <el-descriptions-item label="总条数">
                                    <el-text type="info">{{ unpickedTotal }}</el-text>
                                </el-descriptions-item>
                                <el-descriptions-item label="总数量">
                                    <el-text type="warning">{{ unpickedQuantity }}</el-text>
                                </el-descriptions-item>
                            </el-descriptions>
                        </div>
                    </el-card>
                </el-col>
                <!-- å·²æ‹£é€‰åˆ—表 -->
                <el-col :span="12">
                    <el-card class="table-card" shadow="never">
                        <template #header>
                            <div class="card-header">
                                <span class="card-title">
                                    <i class="el-icon-circle-check"></i>
                                    å·²æ‹£é€‰åˆ—表
                                </span>
                                <el-badge :value="pickedCount" class="badge-item" type="success">
                                    <el-button size="small" @click="refreshPickedTable" icon="el-icon-refresh">
                                        åˆ·æ–°
                                    </el-button>
                                </el-badge>
                            </div>
                        </template>
                        <el-table ref="pickedTable" :data="pickedData" height="400" stripe>
                            <el-table-column type="index" label="序号" width="60" align="center" />
                            <el-table-column prop="materielCode" label="物料编码" width="120" />
                            <el-table-column prop="materielName" label="物料名称" show-overflow-tooltip />
                            <el-table-column prop="batchNo" label="批次号" width="100" />
                            <el-table-column prop="changeQuantity" label="拣选数量" width="80" align="right">
                                <template #default="scope">
                                    <el-text type="success">{{ 0 - scope.row.changeQuantity }}</el-text>
                                </template>
                            </el-table-column>
                            <el-table-column prop="changeQuantity" label="原库存量" width="80" align="right">
                                <template #default="scope">
                                    <el-text type="success">{{ scope.row.beforeQuantity }}</el-text>
                                </template>
                            </el-table-column>
                            <el-table-column prop="changeQuantity" label="拣选后库存量" width="80" align="right">
                                <template #default="scope">
                                    <el-text type="success">{{ scope.row.afterQuantity }}</el-text>
                                </template>
                            </el-table-column>
                            <el-table-column prop="unit" label="单位" width="60" />
                            <el-table-column prop="palletCode" label="托盘码" width="100" />
                            <el-table-column prop="createDate" label="拣选时间" width="160" />
                            <el-table-column prop="originalBarcode" label="原物料码" width="160" />
                            <el-table-column prop="newBarcode" label="新物料码" width="160" />
                            <!-- <el-table-column label="操作" width="80" align="center">
                                <template #default="scope">
                                    <el-button type="text" size="small" @click="undoPick(scope.row)">
                                        æ’¤é”€
                                    </el-button>
                                </template>
                            </el-table-column> -->
                        </el-table>
                        <div class="table-footer">
                            <el-descriptions :column="2" size="small">
                                <el-descriptions-item label="总条数">
                                    <el-text type="info">{{ pickedTotal }}</el-text>
                                </el-descriptions-item>
                                <el-descriptions-item label="总数量">
                                    <el-text type="success">{{ pickedQuantity }}</el-text>
                                </el-descriptions-item>
                            </el-descriptions>
                        </div>
                    </el-card>
                </el-col>
            </el-row>
        </div>
        <!-- ç¡®è®¤å¯¹è¯æ¡† -->
        <el-dialog v-model="confirmDialogVisible" title="操作确认" width="400px" :before-close="handleDialogClose">
            <div class="confirm-content">
                <p>{{ confirmMessage }}</p>
            </div>
            <template #footer>
                <span class="dialog-footer">
                    <el-button @click="confirmDialogVisible = false">取消</el-button>
                    <el-button type="primary" @click="executeConfirm" :loading="executeLoading">
                        ç¡®å®š
                    </el-button>
                </span>
            </template>
        </el-dialog>
    </div>
</template>
<script>
export default {
    name: 'OutPicking',
    data() {
        return {
            orderNo: '',
            orderInfo: null,
            scanForm: {
                palletCode: '',
                materialBarcode: ''
            },
            scanRules: {
                palletCode: [
                    { required: true, message: '请扫描托盘码', trigger: 'blur' }
                ],
                materialBarcode: [
                    { required: true, message: '请扫描物料条码', trigger: 'blur' }
                ]
            },
            unpickedData: [],
            pickedData: [],
            unpickedCount: 0,
            unpickedTotal: 0,
            unpickedQuantity: 0,
            pickedCount: 0,
            pickedTotal: 0,
            pickedQuantity: 0,
            confirmLoading: false,
            confirmDialogVisible: false,
            confirmMessage: '',
            currentAction: null,
            executeLoading: false
        }
    },
    computed: {
        canConfirm() {
            return this.scanForm.palletCode && this.scanForm.materialBarcode
        }
    },
    mounted() {
        this.initPage()
    },
    methods: {
        initPage() {
            // ä»Žè·¯ç”±å‚数获取订单号
            this.orderNo = this.$route.query.orderNo || ''
            if (!this.orderNo) {
                this.$message.error('订单号不能为空')
                this.$router.back()
                return
            }
            // è‡ªåŠ¨èšç„¦åˆ°æ‰˜ç›˜ç è¾“å…¥æ¡†
            this.$nextTick(() => {
                if (this.$refs.palletInput) {
                    this.$refs.palletInput.focus()
                }
            })
        },
        loadPalletData() {
            if (!this.scanForm.palletCode) {
                this.unpickedData = []
                return
            }
            try {
                this.loadUnpickedData();
                this.loadPickedData();
            } catch (error) {
                console.error('加载托盘数据失败:', error)
                this.unpickedData = []
            }
        },
        loadUnpickedData() {
            try {
                this.http.post(`/api/Outbound/QueryPickingTasks?orderNo=${this.orderNo}&palletCode=${this.scanForm.palletCode}`, {}).then(response => {
                    if (response.status) {
                        if (response.data.length > 0) {
                            this.unpickedData = response.data
                            this.calculateUnpickedStats()
                            // è‡ªåŠ¨èšç„¦åˆ°ç‰©æ–™æ¡ç è¾“å…¥æ¡†
                            this.$nextTick(() => {
                                if (this.$refs.materialInput) {
                                    this.$refs.materialInput.focus()
                                }
                            })
                        } else {
                            this.$message.warning('该托盘无未拣选任务')
                            this.unpickedData = []
                        }
                    } else {
                        this.$message.error(response.message || '获取托盘数据失败')
                        this.unpickedData = []
                    }
                }
                )
            } catch (error) {
                console.error('加载未拣选数据失败:', error)
            }
        },
        loadPickedData() {
            try {
                this.http.post(`/api/Outbound/QueryPickedList?orderNo=${this.orderNo}&palletCode=${this.scanForm.palletCode}`, {}).then(response => {
                    if (response.status) {
                        if (response.data.length > 0) {
                            this.pickedData = response.data
                            this.calculatePickedStats()
                        } else {
                            this.$message.warning('该托盘无未拣选任务')
                            this.unpickedData = []
                        }
                    } else {
                        this.$message.error(response.message || '获取托盘数据失败')
                        this.unpickedData = []
                    }
                }
                )
            } catch (error) {
                console.error('加载已拣选数据失败:', error)
            }
        },
        // è®¡ç®—未拣选
        calculateUnpickedStats() {
            // æœªæ‹£é€‰æ¡ç›®æ•°é‡
            this.unpickedCount = this.unpickedData.length
            // è®¡ç®—未拣选的总数量(分拣数量 - å·²åˆ†æ‹£æ•°é‡ï¼‰
            this.unpickedQuantity = this.unpickedData.reduce((sum, item) => {
                const assignQty = item.assignQuantity || 0
                const sortedQty = item.sortedQuantity || 0
                const remainingQty = Math.max(0, assignQty - sortedQty)
                return sum + remainingQty
            }, 0)
            // æ€»æ¡ç›®æ•°ï¼ˆä¸Žæ¡ç›®æ•°é‡ç›¸åŒï¼Œè¿™é‡Œä¿ç•™å˜é‡ä¸€è‡´æ€§ï¼‰
            this.unpickedTotal = this.unpickedCount
        },
        // è®¡ç®—已拣选
        calculatePickedStats() {
            // å·²æ‹£é€‰æ¡ç›®æ•°é‡
            this.pickedCount = this.pickedData.length
            // è®¡ç®—已拣选的总数量
            this.pickedQuantity = 0 - this.pickedData.reduce((sum, item) => {
                return (sum + item.changeQuantity)
            }, 0)
            // æ€»æ¡ç›®æ•°ï¼ˆä¸Žæ¡ç›®æ•°é‡ç›¸åŒï¼‰
            this.pickedTotal = this.pickedCount
        },
        handlePalletScan() {
            if (this.scanForm.palletCode) {
                // this.$message.success(`托盘码: ${this.scanForm.palletCode}`)
                this.loadPalletData()
            }
        },
        handleMaterialScan() {
            if (!this.scanForm.palletCode) {
                this.$message.warning('请先扫描托盘码')
                this.$refs.palletInput.focus()
                return
            }
            if (!this.scanForm.materialBarcode) {
                this.$message.warning('请扫描物料条码')
                return
            }
            // è‡ªåŠ¨æ‰§è¡Œç¡®è®¤æ‹£é€‰
            this.handleConfirmPick()
        },
        handleConfirmPick() {
            if (!this.scanForm.palletCode || !this.scanForm.materialBarcode) {
                this.$message.warning('请先扫描托盘码和物料条码')
                return
            }
            this.confirmLoading = true
            try {
                this.http.post('/api/Outbound/CompleteOutboundWithBarcode', {
                    orderNo: this.orderNo,
                    palletCode: this.scanForm.palletCode,
                    barcode: this.scanForm.materialBarcode,
                    operator: this.getUserName()
                }).then(response => {
                    if (response.status) {
                        this.$message.success('拣选确认成功')
                        this.resetMaterialBarcode()
                        // this.loadUnpickedData()
                        // this.loadPickedData()
                        this.loadPalletData()
                    } else {
                        this.$message.error(response.message || '拣选确认失败')
                    }
                })
            } catch (error) {
                console.error('拣选确认失败:', error)
                this.$message.error('拣选确认失败')
            } finally {
                this.confirmLoading = false
            }
        },
        handleEmptyBox() {
            if (!this.scanForm.palletCode) {
                this.$message.warning('请先扫描托盘码')
                return
            }
            this.confirmMessage = `确定要取空托盘 ${this.scanForm.palletCode} å—?`
            this.currentAction = 'emptyBox'
            this.confirmDialogVisible = true
        },
        handleReturnToWarehouse() {
            if (!this.scanForm.palletCode) {
                this.$message.warning('请先扫描托盘码')
                return
            }
            this.confirmMessage = `确定要将托盘 ${this.scanForm.palletCode} å›žåº“吗?`
            this.currentAction = 'returnToWarehouse'
            this.confirmDialogVisible = true
        },
        executeConfirm() {
            this.executeLoading = true
            try {
                let apiUrl = ''
                let params = {
                    orderNo: this.orderNo,
                    palletCode: this.scanForm.palletCode
                }
                if (this.currentAction === 'emptyBox') {
                    apiUrl = '/api/Outbound/EmptyBox'
                } else if (this.currentAction === 'returnToWarehouse') {
                    apiUrl = '/api/Outbound/ReturnToWarehouse'
                }
                this.http.post(apiUrl, params).then(response => {
                    if (response.status) {
                        this.$message.success('操作成功')
                        this.confirmDialogVisible = false
                        this.resetForm()
                        // this.loadUnpickedData()
                        // this.loadPickedData()
                    } else {
                        this.$message.error(response.message || '操作失败')
                    }
                })
            } catch (error) {
                console.error('操作失败:', error)
                this.$message.error('操作失败')
            } finally {
                this.executeLoading = false
            }
        },
        handleDialogClose() {
            if (!this.executeLoading) {
                this.confirmDialogVisible = false
            }
        },
        quickPick(row) {
            this.scanForm.materialBarcode = row.materielCode
            this.handleConfirmPick()
        },
        undoPick(row) {
            try {
                this.$confirm('确定要撤销这条拣选记录吗?', '提示', {
                    type: 'warning'
                })
                const response = this.http.post('/api/Outbound/UndoPicking', {
                    id: row.id
                }).then(response => {
                    if (response.status) {
                        this.$message.success('撤销成功')
                        this.loadUnpickedData()
                        this.loadPickedData()
                    } else {
                        this.$message.error(response.message || '撤销失败')
                    }
                })
            } catch (error) {
                if (error !== 'cancel') {
                    console.error('撤销失败:', error)
                    this.$message.error('撤销失败')
                }
            }
        },
        // handleUnpickedRowClick(row) {
        //     // ç‚¹å‡»æœªæ‹£é€‰è¡Œæ—¶è‡ªåŠ¨å¡«å……ç‰©æ–™æ¡ç 
        //     this.scanForm.materialBarcode = row.materielCode
        // },
        refreshUnpickedTable() {
            if (this.scanForm.palletCode) {
                this.loadPalletData()
            }
        },
        refreshPickedTable() {
            this.loadPickedData()
        },
        resetMaterialBarcode() {
            this.scanForm.materialBarcode = ''
            this.$nextTick(() => {
                if (this.$refs.materialInput) {
                    this.$refs.materialInput.focus()
                }
            })
        },
        resetForm() {
            this.scanForm.palletCode = ''
            this.scanForm.materialBarcode = ''
            this.unpickedData = []
            this.$nextTick(() => {
                if (this.$refs.palletInput) {
                    this.$refs.palletInput.focus()
                }
            })
        },
        getUserName() {
            // å°è¯•从 Vuex store èŽ·å–ç”¨æˆ·å
            if (this.$store && this.$store.state && this.$store.state.userInfo) {
                return this.$store.state.userInfo.userName || this.$store.state.userInfo.username || '未登录用户'
            }
            // å°è¯•从 localStorage èŽ·å–
            try {
                const userInfo = localStorage.getItem('user')
                if (userInfo) {
                    const user = JSON.parse(userInfo)
                    return user.userName || user.username || '未登录用户'
                }
            } catch (error) {
                console.error('获取用户信息失败:', error)
            }
            return '未登录用户'
        },
        getStatusType(status) {
            const statusMap = {
                0: 'info',    // å¾…处理
                10: 'warning', // è¿›è¡Œä¸­
                20: 'primary', // æ‹£é€‰ä¸­
                30: 'success', // å·²å®Œæˆ
                40: 'danger'   // å¼‚常
            }
            return statusMap[status] || 'info'
        }
    }
}
</script>
<style scoped>
.picking-container {
    padding: 20px;
    background-color: #f5f5f5;
    min-height: 100vh;
}
/* è®¢å•信息卡片 */
.order-info-card {
    margin-bottom: 20px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
}
.order-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.order-title {
    display: flex;
    align-items: center;
    font-size: 18px;
    font-weight: bold;
}
.order-title i {
    margin-right: 10px;
    font-size: 24px;
}
.order-label {
    margin-left: 10px;
    opacity: 0.9;
}
.order-value {
    font-size: 20px;
    font-weight: bold;
    letter-spacing: 1px;
}
/* æ‰«ç åŒºåŸŸ */
.scan-section-card {
    margin-bottom: 20px;
}
.scan-section {
    padding: 10px 0;
}
.scan-alert {
    margin-bottom: 20px;
}
.scan-form {
    margin-top: 20px;
}
.button-form-item {
    margin-bottom: 0;
}
.button-form-item .el-form-item__content {
    line-height: normal;
}
.action-buttons {
    display: flex;
    gap: 8px;
    width: 100%;
    height: 40px;
}
.action-buttons .el-button {
    flex: 1;
    height: 40px;
    font-weight: bold;
    border-radius: 6px;
    margin: 0;
}
/* è¡¨æ ¼åŒºåŸŸ */
.tables-section {
    margin-top: 20px;
}
.table-card {
    height: 600px;
    display: flex;
    flex-direction: column;
}
.card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.card-title {
    display: flex;
    align-items: center;
    font-weight: bold;
    font-size: 16px;
}
.card-title i {
    margin-right: 8px;
    color: #409EFF;
}
.badge-item {
    margin-left: 10px;
}
.table-footer {
    margin-top: 10px;
    padding-top: 10px;
    border-top: 1px solid #ebeef5;
}
/* è¡¨æ ¼è¡Œæ ·å¼ */
.el-table tbody tr:hover>td {
    background-color: #f0f9ff !important;
}
.el-table tbody tr.current-row>td {
    background-color: #e1f3ff !important;
}
/* å¯¹è¯æ¡†æ ·å¼ */
.confirm-content {
    padding: 20px 0;
    text-align: center;
}
.confirm-content p {
    font-size: 16px;
    color: #606266;
}
.dialog-footer {
    text-align: center;
}
/* å“åº”式设计 */
@media (max-width: 1200px) {
    .action-buttons .el-button {
        font-size: 14px;
    }
}
/* Element UI ç»„件样式覆盖 */
::v-deep .el-card__header {
    padding: 18px 20px;
    border-bottom: 1px solid #ebeef5;
}
::v-deep .el-input__inner {
    border-radius: 6px;
}
::v-deep .el-button--primary {
    background: linear-gradient(135deg, #409EFF 0%, #3a8ee6 100%);
    border: none;
}
::v-deep .el-button--warning {
    background: linear-gradient(135deg, #E6A23C 0%, #d9971a 100%);
    border: none;
}
::v-deep .el-button--success {
    background: linear-gradient(135deg, #67C23A 0%, #5daf34 100%);
    border: none;
}
/* å›¾æ ‡æ ·å¼ */
.el-icon-document,
.el-icon-box,
.el-icon-s-grid,
.el-icon-check,
.el-icon-delete,
.el-icon-refresh-left,
.el-icon-time,
.el-icon-circle-check {
    font-size: 18px;
}
/* æè¿°åˆ—表样式 */
::v-deep .el-descriptions__label {
    font-weight: bold;
    color: #909399;
}
::v-deep .el-descriptions__content {
    color: #606266;
}
</style>
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/17.14.1204.46620/CodeChunks.db
Binary files differ
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/17.14.1204.46620/SemanticSymbols.db
Binary files differ
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_DTO/CalcOut/MaterielOutboundCalculationDTO.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WIDESEA_Model.Models;
namespace WIDESEA_DTO.CalcOut
{
    /// <summary>
    /// æŒ‰ç‰©æ–™çš„出库计算结果
    /// </summary>
    public class MaterielOutboundCalculationDTO
    {
        /// <summary>
        /// ç‰©æ–™ç¼–号
        /// </summary>
        public string MaterielCode { get; set; }
        /// <summary>
        /// ç‰©æ–™åç§°
        /// </summary>
        public string MaterielName { get; set; }
        /// <summary>
        /// æ‰¹æ¬¡å·
        /// </summary>
        public string BatchNo { get; set; }
        /// <summary>
        /// ä¾›åº”商编号(可能为空)
        /// </summary>
        public string SupplyCode { get; set; }
        /// <summary>
        /// ä»“库编号(可能为空)
        /// </summary>
        public string WarehouseCode { get; set; }
        /// <summary>
        /// è¯¥ç‰©æ–™çš„æ€»å•据数量
        /// </summary>
        public decimal TotalOrderQuantity { get; set; }
        /// <summary>
        /// è¯¥ç‰©æ–™çš„æ€»å·²å‡ºæ•°é‡
        /// </summary>
        public decimal TotalOverOutQuantity { get; set; }
        /// <summary>
        /// è¯¥ç‰©æ–™çš„已分配数量
        /// </summary>
        public decimal AssignedQuantity { get; set; }
        /// <summary>
        /// è¯¥ç‰©æ–™çš„待分配数量(已减去挪料数量)
        /// </summary>
        public decimal UnallocatedQuantity { get; set; }
        /// <summary>
        /// æŒªæ–™æ•°é‡
        /// </summary>
        public decimal MovedQuantity { get; set; }
        /// <summary>
        /// è¯¥ç‰©æ–™å¯¹åº”的出库明细列表
        /// </summary>
        public List<Dt_OutboundOrderDetail> Details { get; set; } = new List<Dt_OutboundOrderDetail>();
        /// <summary>
        ///
        /// </summary>
        public List<Dt_OutStockLockInfo> OutStockLockInfos { get; set; } = new List<Dt_OutStockLockInfo>();
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_DTO/CalcOut/OutboundCalculationDTO.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WIDESEA_Model.Models;
namespace WIDESEA_DTO.CalcOut
{
    /// <summary>
    /// å‡ºåº“计算结果DTO
    /// </summary>
    public class OutboundCalculationDTO
    {
        /// <summary>
        /// æ˜¯å¦å…è®¸å‡ºåº“
        /// </summary>
        public bool CanOutbound { get; set; }
        /// <summary>
        /// é”™è¯¯æ¶ˆæ¯
        /// </summary>
        public string ErrorMessage { get; set; }
        /// <summary>
        /// å‡ºåº“单信息
        /// </summary>
        public Dt_OutboundOrder OutboundOrder { get; set; }
        /// <summary>
        /// é€‰æ‹©çš„出库明细列表
        /// </summary>
        public List<Dt_OutboundOrderDetail> SelectedDetails { get; set; }
        /// <summary>
        /// æŒ‰ç‰©æ–™åˆ†ç»„的出库计算结果
        /// </summary>
        public List<MaterielOutboundCalculationDTO> MaterielCalculations { get; set; } = new List<MaterielOutboundCalculationDTO>();
        /// åŽ‚åŒº
        /// </summary>
        public string FactoryArea { get; set; }
        /// <summary>
        /// æ˜¯å¦å¤šæ˜Žç»†å‡ºåº“
        /// </summary>
        public bool IsMultiDetail { get; set; }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_DTO/CalcOut/OutboundCompleteRequestDTO.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WIDESEA_DTO.CalcOut
{
    /// <summary>
    /// å‡ºåº“完成处理请求DTO
    /// </summary>
    public class OutboundCompleteRequestDTO
    {
        /// <summary>
        /// å‡ºåº“单编号
        /// </summary>
        [Required(ErrorMessage = "出库单编号不能为空")]
        public string OrderNo { get; set; }
        /// <summary>
        ///
        /// </summary>
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
        /// <summary>
        /// æ‰«æçš„æ¡ç 
        /// </summary>
        [Required(ErrorMessage = "条码不能为空")]
        public string Barcode { get; set; }
        /// <summary>
        /// æ“ä½œè€…
        /// </summary>
        public string Operator { get; set; }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_DTO/CalcOut/OutboundCompleteResponseDTO.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WIDESEA_Model.Models;
namespace WIDESEA_DTO.CalcOut
{
    /// <summary>
    /// å‡ºåº“完成处理响应DTO
    /// </summary>
    public class OutboundCompleteResponseDTO
    {
        /// <summary>
        /// æ˜¯å¦æˆåŠŸ
        /// </summary>
        public bool Success { get; set; }
        /// <summary>
        /// æ¶ˆæ¯
        /// </summary>
        public string Message { get; set; }
        /// <summary>
        /// æ‰«æçš„库存明细信息
        /// </summary>
        public ScannedStockDetailDTO ScannedDetail { get; set; }
        /// <summary>
        /// æ›´æ–°åŽçš„出库单明细信息
        /// </summary>
        public List<Dt_OutboundOrderDetail> UpdatedDetails { get; set; }
        /// <summary>
        /// é‡æ–°ç”Ÿæˆçš„æ¡ç ï¼ˆæ‹†åŒ…时)
        /// </summary>
        public string NewBarcode { get; set; }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_DTO/CalcOut/PickedListItemDTO.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WIDESEA_DTO.CalcOut
{
    public class PickedListItemDTO
    {
        public int Id { get; set; }
        public string OrderNo { get; set; }
        public int? TaskNum { get; set; }
        public string PalletCode { get; set; }
        public string MaterielCode { get; set; }
        public string MaterielName { get; set; }
        public string BatchNo { get; set; }
        public string OriginalBarcode { get; set; }
        public string NewBarcode { get; set; }
        public decimal ChangeQuantity { get; set; }
        public decimal BeforeQuantity { get; set; }
        public decimal AfterQuantity { get; set; }
        public string SupplyCode { get; set; }
        public string WarehouseCode { get; set; }
        public string Remark { get; set; }
        public DateTime CreateDate { get; set; }
        public int? OrderStatus { get; set; }
        public string FactoryArea { get; set; }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_DTO/CalcOut/PickedStockDetailDTO.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WIDESEA_Model.Models;
namespace WIDESEA_DTO.CalcOut
{
    /// <summary>
    /// å·²åˆ†æ‹£åº“存明细DTO
    /// </summary>
    public class PickedStockDetailDTO
    {
        /// <summary>
        /// æ‰˜ç›˜ç¼–号
        /// </summary>
        public string PalletCode { get; set; }
        /// <summary>
        /// ç‰©æ–™ç¼–号
        /// </summary>
        public string MaterielCode { get; set; }
        /// <summary>
        /// æ‰¹æ¬¡å·
        /// </summary>
        public string BatchNo { get; set; }
        /// <summary>
        /// æœ¬æ¬¡å‡ºåº“数量
        /// </summary>
        public decimal OutboundQuantity { get; set; }
        /// <summary>
        /// å‰©ä½™åº“存数量
        /// </summary>
        public decimal RemainingQuantity { get; set; }
        /// <summary>
        ///
        /// </summary>
        public string LocationCode { get; set; }
        /// <summary>
        ///
        /// </summary>
        public List<Dt_OutStockLockInfo> OutStockLockInfos { get; set; } = new List<Dt_OutStockLockInfo>();
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_DTO/CalcOut/PickingOutboundRequestDTO.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WIDESEA_DTO.CalcOut
{
    /// <summary>
    /// åˆ†æ‹£å‡ºåº“请求DTO
    /// </summary>
    public class PickingOutboundRequestDTO
    {
        /// <summary>
        /// å‡ºåº“单编号
        /// </summary>
        [Required(ErrorMessage = "出库单编号不能为空")]
        public string OrderNo { get; set; }
        /// <summary>
        /// é€‰æ‹©çš„出库明细ID列表(单个或多个明细)
        /// </summary>
        [Required(ErrorMessage = "请选择要出库的明细")]
        public List<int> DetailIds { get; set; } = new List<int>();
        /// <summary>
        /// æœ¬æ¬¡å‡ºåº“数量(单个明细时需要填写,多个明细时为空)
        /// </summary>
        [Range(0.0001, double.MaxValue, ErrorMessage = "出库数量必须大于0")]
        public decimal? OutboundQuantity { get; set; }
        /// <summary>
        /// æ“ä½œè€…
        /// </summary>
        public string Operator { get; set; }
        /// <summary>
        /// å‡ºåº“目标位置
        /// </summary>
        public string OutboundTargetLocation { get; set; }
        /// <summary>
        /// æ˜¯å¦å¤šæ˜Žç»†å‡ºåº“(通过DetailIds数量判断,也可手动指定)
        /// </summary>
        public bool IsMultiDetail => DetailIds?.Count > 1;
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_DTO/CalcOut/PickingOutboundResponseDTO.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WIDESEA_Model.Models;
namespace WIDESEA_DTO.CalcOut
{
    /// <summary>
    /// åˆ†æ‹£å‡ºåº“响应DTO
    /// </summary>
    public class PickingOutboundResponseDTO
    {
        /// <summary>
        /// æ˜¯å¦æˆåŠŸ
        /// </summary>
        public bool Success { get; set; }
        /// <summary>
        /// æ¶ˆæ¯
        /// </summary>
        public string Message { get; set; }
        /// <summary>
        /// å‡ºåº“单状态
        /// </summary>
        public int OrderStatus { get; set; }
        /// <summary>
        /// æœ¬æ¬¡å‡ºåº“的库存明细信息
        /// </summary>
        public List<PickedStockDetailDTO> PickedDetails { get; set; }
        /// <summary>
        /// ä»»åŠ¡é›†åˆ
        /// </summary>
        public List<Dt_Task> Tasks { get; set; }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_DTO/CalcOut/ScannedStockDetailDTO.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WIDESEA_DTO.CalcOut
{
    /// <summary>
    /// æ‰«æçš„库存明细DTO
    /// </summary>
    public class ScannedStockDetailDTO
    {
        /// <summary>
        /// åº“存明细ID
        /// </summary>
        public int StockDetailId { get; set; }
        /// <summary>
        /// æ‰˜ç›˜ç¼–号
        /// </summary>
        public string PalletCode { get; set; }
        /// <summary>
        /// ç‰©æ–™ç¼–号
        /// </summary>
        public string MaterielCode { get; set; }
        /// <summary>
        /// ç‰©æ–™åç§°
        /// </summary>
        public string MaterielName { get; set; }
        /// <summary>
        /// æ‰¹æ¬¡å·
        /// </summary>
        public string BatchNo { get; set; }
        /// <summary>
        /// åŽŸå§‹æ¡ç 
        /// </summary>
        public string OriginalBarcode { get; set; }
        /// <summary>
        /// å˜åŠ¨å‰æ•°é‡
        /// </summary>
        public decimal BeforeQuantity { get; set; }
        /// <summary>
        /// å˜åŠ¨åŽæ•°é‡
        /// </summary>
        public decimal AfterQuantity { get; set; }
        /// <summary>
        /// å˜åŠ¨æ•°é‡
        /// </summary>
        public decimal ChangeQuantity { get; set; }
        /// <summary>
        /// æ˜¯å¦æ‹†åŒ…
        /// </summary>
        public bool IsUnpacked { get; set; }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_IOutboundService/IOutboundService.cs
@@ -4,6 +4,7 @@
using System.Text;
using System.Threading.Tasks;
using WIDESEA_Core;
using WIDESEA_DTO.CalcOut;
namespace WIDESEA_IOutboundService
{
@@ -14,5 +15,23 @@
        IOutboundOrderService OutboundOrderService { get; }
        IOutStockLockInfoService OutboundStockLockInfoService { get; }
        /// <summary>
        /// åˆ†æ‹£å‡ºåº“操作
        /// </summary>
        /// <param name="request">分拣出库请求</param>
        /// <returns>分拣出库响应</returns>
        WebResponseContent ProcessPickingOutbound(PickingOutboundRequestDTO request);
        /// <summary>
        /// å‡ºåº“完成处理(扫描条码扣减库存)
        /// </summary>
        /// <param name="request">出库完成请求</param>
        /// <returns>出库完成响应</returns>
        WebResponseContent CompleteOutboundWithBarcode(OutboundCompleteRequestDTO request);
        WebResponseContent QueryPickingTasks(string palletCode, string orderNo);
        WebResponseContent QueryPickedList(string orderNo, string palletCode);
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Model/Models/Outbound/Dt_OutboundLockInfo.cs
@@ -159,5 +159,32 @@
        
        public int ReturnToMESStatus { get; set; }
        //新增字段
        /// <summary>
        /// æœ¬æ¬¡åˆ†é…å‰çš„累计量
        /// </summary>
        [SugarColumn(IsNullable = false, ColumnDescription = "本次分配前的累计量")]
        public decimal AllocatedQuantity { get; set; } = 0;
        /// <summary>
        /// å‡ºåº“目标位置
        /// </summary>
        [SugarColumn(IsNullable = true, Length = 100, ColumnDescription = "出库目标位置")]
        public string OutboundTargetLocation { get; set; }
        /// <summary>
        /// å•据明细主键(支持多个明细ID,逗号分隔)
        /// </summary>
        [SugarColumn(IsNullable = true, Length = 500, ColumnDescription = "单据明细主键")]
        public string OrderDetailIds { get; set; }
        /// <summary>
        ///
        /// </summary>
        [SugarColumn(IsNullable = true, ColumnDescription = "分拣完成数量")]
        public decimal? SortedQuantity { get; set; } = 0;
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Model/Models/Record/Dt_StockQuantityChangeRecord.cs
@@ -118,5 +118,20 @@
        /// </summary>
        [SugarColumn(IsNullable = true, ColumnDescription = "备注")]
        public string Remark { get; set; }
        //新增字段
        /// <summary>
        /// åŽŸå§‹åºåˆ—å·/物料码
        /// </summary>
        [SugarColumn(IsNullable = false, Length = 100, ColumnDescription = "原始序列号")]
        public string OriginalSerilNumber { get; set; }
        /// <summary>
        /// æ–°åºåˆ—号
        /// </summary>
        [SugarColumn(IsNullable = false, Length = 100, ColumnDescription = "新序列号")]
        public string NewSerilNumber { get; set; }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundQueryService.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SqlSugar;
using WIDESEA_Common.StockEnum;
using WIDESEA_Core;
using WIDESEA_DTO.CalcOut;
using WIDESEA_Model.Models;
namespace WIDESEA_OutboundService
{
    public partial class OutboundService
    {
        public WebResponseContent QueryPickingTasks(string palletCode, string orderNo)
        {
            try
            {
                List<Dt_OutStockLockInfo> outStockLockInfos = _outboundLockInfoService.Repository.QueryData(x => x.PalletCode == palletCode && x.OrderNo == orderNo);
                return WebResponseContent.Instance.OK(data: outStockLockInfos);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error(ex.Message);
            }
        }
        /// <summary>
        /// æŸ¥è¯¢å·²æ‹£é€‰åˆ—表(通过库存变动记录)
        /// </summary>
        /// <param name="request">查询请求</param>
        /// <returns>已拣选列表</returns>
        public WebResponseContent QueryPickedList(string orderNo, string palletCode)
        {
            WebResponseContent content = WebResponseContent.Instance;
            try
            {
                // æž„建查询条件
                var query = _stockChangeService.Repository.Db
                    .Queryable<Dt_StockQuantityChangeRecord>()
                    .LeftJoin<Dt_OutboundOrder>((r, o) => r.OrderNo == o.OrderNo)
                    .Where((r, o) => r.ChangeType == (int)StockChangeTypeEnum.Outbound)
                    .Where((r, o) => r.OrderNo == orderNo)
                    .Where((r, o) => r.PalleCode == palletCode)
                    .OrderBy((r, o) => r.CreateDate, OrderByType.Desc)
                    .Select((r, o) => new PickedListItemDTO
                    {
                        Id = r.Id,
                        OrderNo = r.OrderNo,
                        TaskNum = r.TaskNum,
                        PalletCode = r.PalleCode,
                        MaterielCode = r.MaterielCode,
                        MaterielName = r.MaterielName,
                        BatchNo = r.BatchNo,
                        OriginalBarcode = r.OriginalSerilNumber,
                        NewBarcode = r.NewSerilNumber,
                        ChangeQuantity = r.ChangeQuantity,
                        BeforeQuantity = r.BeforeQuantity,
                        AfterQuantity = r.AfterQuantity,
                        SupplyCode = r.SupplyCode,
                        WarehouseCode = r.WarehouseCode,
                        Remark = r.Remark,
                        CreateDate = r.CreateDate,
                        OrderStatus = o.OrderStatus,
                        FactoryArea = o.FactoryArea
                    });
                var result = query.ToList();
                return WebResponseContent.Instance.OK(data: result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"查询已拣选列表失败:{ex.Message}");
            }
        }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundService.cs
@@ -1,9 +1,24 @@
using WIDESEA_IOutboundService;
using SqlSugar;
using WIDESEA_Common.LocationEnum;
using WIDESEA_Common.OrderEnum;
using WIDESEA_Common.StockEnum;
using WIDESEA_Common.TaskEnum;
using WIDESEA_Core;
using WIDESEA_Core.BaseRepository;
using WIDESEA_Core.Helper;
using WIDESEA_DTO.CalcOut;
using WIDESEA_IBasicService;
using WIDESEA_IOutboundService;
using WIDESEA_IRecordService;
using WIDESEA_IStockService;
using WIDESEA_Model.Models;
namespace WIDESEA_OutboundService
{
    public class OutboundService : IOutboundService
    public partial class OutboundService : IOutboundService
    {
        public IUnitOfWorkManage _unitOfWorkManage { get; }
        public IOutboundOrderDetailService OutboundOrderDetailService { get; }
@@ -11,11 +26,1130 @@
        public IOutStockLockInfoService OutboundStockLockInfoService { get; }
        public OutboundService(IOutboundOrderDetailService outboundOrderDetailService, IOutboundOrderService outboundOrderService, IOutStockLockInfoService outboundStockLockInfoService)
        private readonly ISqlSugarClient Db;
        private readonly IOutboundOrderDetailService _detailService;
        private readonly IOutboundOrderService _outboundOrderService;
        private readonly IOutStockLockInfoService _outboundLockInfoService;
        private readonly IStockInfoService _stockInfoService;
        private readonly IStockInfoDetailService _stockDetailService;
        private readonly ILocationInfoService _locationInfoService;
        private readonly IStockQuantityChangeRecordService _stockChangeService;
        private readonly IStockInfoDetail_HtyService _stockDetailHistoryService;
        public OutboundService(IUnitOfWorkManage unitOfWorkManage, IOutboundOrderDetailService outboundOrderDetailService, IOutboundOrderService outboundOrderService, IOutStockLockInfoService outboundStockLockInfoService, IStockInfoService stockInfoService, IStockInfoDetailService stockDetailService, ILocationInfoService locationInfoService, IStockQuantityChangeRecordService stockQuantityChangeRecordService, IStockInfoDetail_HtyService stockDetailHistoryService)
        {
            _unitOfWorkManage = unitOfWorkManage;
            Db = _unitOfWorkManage.GetDbClient();
            OutboundOrderDetailService = outboundOrderDetailService;
            OutboundOrderService = outboundOrderService;
            OutboundStockLockInfoService = outboundStockLockInfoService;
            _detailService = outboundOrderDetailService;
            _outboundOrderService = outboundOrderService;
            _outboundLockInfoService = outboundStockLockInfoService;
            _stockInfoService = stockInfoService;
            _stockDetailService = stockDetailService;
            _locationInfoService = locationInfoService;
            _stockChangeService = stockQuantityChangeRecordService;
            _stockDetailHistoryService = stockDetailHistoryService;
        }
        /// <summary>
        /// åˆ†æ‹£å‡ºåº“操作
        /// </summary>
        /// <param name="request">分拣出库请求</param>
        /// <returns>分拣出库响应</returns>
        public WebResponseContent ProcessPickingOutbound(PickingOutboundRequestDTO request)
        {
            WebResponseContent content = WebResponseContent.Instance;
            PickingOutboundResponseDTO response = new PickingOutboundResponseDTO();
            try
            {
                // 1. è®¡ç®—出库数量逻辑
                OutboundCalculationDTO calculationResult = CalcOutboundQuantity(request);
                if (!calculationResult.CanOutbound)
                {
                    content = WebResponseContent.Instance.Error("无法处理拣货出库:" + calculationResult.ErrorMessage);
                    _unitOfWorkManage.RollbackTran();
                    return content;
                }
                // 2. è°ƒç”¨å‡ºåº“处理逻辑,锁定库存,生成出库记录等
                List<PickedStockDetailDTO> pickedDetails = new List<PickedStockDetailDTO>();
                // èŽ·å–å‡ºåº“å•ä¿¡æ¯
                Dt_OutboundOrder outboundOrder = calculationResult.OutboundOrder;
                // å‡ºåº“详情添加或修改集合
                List<Dt_OutStockLockInfo> outStockLockInfos = new List<Dt_OutStockLockInfo>();
                List<Dt_OutboundOrderDetail> outboundOrderDetails = new();
                List<Dt_Task> tasks = new List<Dt_Task>();
                foreach (var materielCalc in calculationResult.MaterielCalculations)
                {
                    (List<PickedStockDetailDTO> PickedDetails, List<Dt_Task> Tasks, List<Dt_OutStockLockInfo> OutStockLockInfo) materielPickedDetails = ProcessMaterielTaskGeneration(outboundOrder, materielCalc, request, calculationResult.FactoryArea);
                    foreach (var item in materielPickedDetails.OutStockLockInfo)
                    {
                        Dt_OutStockLockInfo? outStockLockInfo = materielCalc.OutStockLockInfos.FirstOrDefault(x => x.Id == item.Id && x.Id > 0);
                        if (outStockLockInfo != null)
                        {
                            outStockLockInfo = item;
                        }
                        else
                        {
                            materielCalc.OutStockLockInfos.Add(item);
                        }
                        outStockLockInfos.Add(item);
                    }
                    tasks.AddRange(materielPickedDetails.Tasks);
                    pickedDetails.AddRange(materielPickedDetails.PickedDetails);
                    // æ›´æ–°å‡ºåº“单明细(增加锁定数量,不增加已出数量)
                    foreach (var detail in materielCalc.Details)
                    {
                        decimal lockQuantity = (detail.OrderQuantity - detail.OverOutQuantity);
                        detail.LockQuantity += lockQuantity; // å¢žåŠ é”å®šæ•°é‡ ä¸æ›´æ–° OverOutQuantity å’Œ OrderDetailStatus,因为还没有实际出库
                        outboundOrderDetails.Add(detail);
                    }
                }
                // 3. æ›´æ–°å‡ºåº“单状态为出库中(表示已有任务分配)
                UpdateOutboundOrderStatus(request.OrderNo, (int)OutOrderStatusEnum.出库中);
                // 4. æ›´æ–°å‡ºåº“单明细锁定数量
                _detailService.Repository.UpdateData(outboundOrderDetails);
                // 5. æ›´æ–°åº“存状态
                UpdateStockStatus(pickedDetails.Select(x => x.PalletCode).ToList(), StockStatusEmun.出库锁定.ObjToInt());
                // 6. æ›´æ–°è´§ä½çŠ¶æ€
                UpdateLocationStatus(pickedDetails.Select(x => x.LocationCode).ToList(), LocationStatusEnum.Lock.ObjToInt());
                // 7. æ›´æ–°åº“存详情
                UpdateOutStockLockInfo(outStockLockInfos);
                _unitOfWorkManage.CommitTran();
                response.Success = true;
                response.Message = "分拣任务分配成功";
                response.Tasks = tasks; // è¿”回第一个任务号
                response.PickedDetails = pickedDetails; // è¿”回第一个分拣明细
                content = WebResponseContent.Instance.OK("分拣任务分配成功", response);
                return content;
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                content = WebResponseContent.Instance.Error("处理拣货出库失败:" + ex.Message);
            }
            return content;
        }
        /// <summary>
        /// è®¡ç®—出库数量逻辑
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        public OutboundCalculationDTO CalcOutboundQuantity(PickingOutboundRequestDTO request)
        {
            OutboundCalculationDTO result = new();
            try
            {
                Dt_OutboundOrder outboundOrder = _outboundOrderService.Repository.QueryFirst(x => x.OrderNo == request.OrderNo);
                if (outboundOrder == null)
                {
                    result.CanOutbound = false;
                    result.ErrorMessage = $"出库单 {request.OrderNo} ä¸å­˜åœ¨";
                    return result;
                }
                result.FactoryArea = outboundOrder.FactoryArea;
                result.IsMultiDetail = request.IsMultiDetail;
                // èŽ·å–é€‰æ‹©çš„å‡ºåº“æ˜Žç»†
                List<Dt_OutboundOrderDetail> selectedDetails = _detailService.Repository.QueryData(x => x.OrderId == outboundOrder.Id && request.DetailIds.Contains(x.Id));
                if (!selectedDetails.Any())
                {
                    result.CanOutbound = false;
                    result.ErrorMessage = $"未找到选择的出库明细信息";
                    return result;
                }
                if (selectedDetails.Any(x => x.LockQuantity > x.OrderQuantity - x.MoveQty || x.OverOutQuantity > x.OrderQuantity - x.MoveQty))
                {
                    List<int> selectDetailIds = selectedDetails.Where(x => x.LockQuantity > x.OrderQuantity - x.MoveQty || x.OverOutQuantity > x.OrderQuantity - x.MoveQty).Select(x => x.Id).ToList();
                    result.CanOutbound = false;
                    result.ErrorMessage = $"出库明细信息{string.Join(",", selectDetailIds)}已分配完成";
                    return result;
                }
                outboundOrder.Details = selectedDetails;
                result.OutboundOrder = outboundOrder;
                result.SelectedDetails = selectedDetails;
                if (request.IsMultiDetail)
                {
                    // å¤šæ˜Žç»†å‡ºåº“:按物料分组处理
                    result.MaterielCalculations = CalcMaterielOutboundQuantities(outboundOrder, selectedDetails.ToList());
                }
                else
                {
                    // å•明细出库:验证输入的出库数量
                    if (!request.OutboundQuantity.HasValue || request.OutboundQuantity.Value <= 0)
                    {
                        result.CanOutbound = false;
                        result.ErrorMessage = "单明细出库时必须指定出库数量且大于0";
                        return result;
                    }
                    Dt_OutboundOrderDetail? singleDetail = selectedDetails.First();
                    //判断可出库数量
                    if (singleDetail.OrderQuantity - singleDetail.LockQuantity - singleDetail.MoveQty <= 0)
                    {
                        result.CanOutbound = false;
                        result.ErrorMessage = $"本次出库数量 {request.OutboundQuantity.Value} è¶…过可出库数量 {singleDetail.OrderQuantity - singleDetail.LockQuantity - singleDetail.MoveQty}";
                        return result;
                    }
                    result.MaterielCalculations = new List<MaterielOutboundCalculationDTO>()
                    {
                        new MaterielOutboundCalculationDTO
                        {
                            MaterielCode = singleDetail.MaterielCode,
                            MaterielName = singleDetail.MaterielName,
                            BatchNo = singleDetail.BatchNo,
                            SupplyCode = singleDetail.SupplyCode,
                            WarehouseCode = singleDetail.WarehouseCode,
                            TotalOrderQuantity = singleDetail.OrderQuantity - singleDetail.MoveQty,
                            TotalOverOutQuantity = singleDetail.OverOutQuantity,
                            AssignedQuantity = singleDetail.LockQuantity,
                            UnallocatedQuantity = singleDetail.OrderQuantity - singleDetail.LockQuantity - singleDetail.MoveQty,
                            MovedQuantity = singleDetail.MoveQty,
                            Details = new List<Dt_OutboundOrderDetail>() { singleDetail }
                        }
                    };
                }
                result.CanOutbound = true;
                return result;
            }
            catch (Exception ex)
            {
                result.CanOutbound = false;
                result.ErrorMessage = ex.Message;
                return result;
            }
        }
        /// <summary>
        /// å¤šå‡ºåº“单明细时,按物料分组计算出库数量
        /// </summary>
        /// <param name="selectedDetails"></param>
        /// <returns></returns>
        private List<MaterielOutboundCalculationDTO> CalcMaterielOutboundQuantities(Dt_OutboundOrder outboundOrder, List<Dt_OutboundOrderDetail> selectedDetails)
        {
            // æŒ‰ç‰©æ–™åˆ†ç»„:物料编号、批次号、供应商编号、仓库编号
            List<MaterielOutboundCalculationDTO> materielGroups = selectedDetails
                .GroupBy(x => new
                {
                    x.MaterielCode,
                    x.MaterielName,
                    x.BatchNo,
                    x.SupplyCode,
                    x.WarehouseCode
                })
                .Select(g => new MaterielOutboundCalculationDTO
                {
                    MaterielCode = g.Key.MaterielCode,
                    MaterielName = g.Key.MaterielName,
                    BatchNo = g.Key.BatchNo,
                    SupplyCode = g.Key.SupplyCode,
                    WarehouseCode = g.Key.WarehouseCode,
                    TotalOrderQuantity = g.Sum(x => x.OrderQuantity - x.MoveQty),
                    TotalOverOutQuantity = g.Sum(x => x.OverOutQuantity),
                    AssignedQuantity = g.Sum(x => x.LockQuantity),
                    UnallocatedQuantity = g.Sum(x => x.OrderQuantity - x.LockQuantity - x.MoveQty),
                    MovedQuantity = g.Sum(x => x.MoveQty),
                    Details = g.ToList(),
                    OutStockLockInfos = _outboundLockInfoService.Repository.QueryData(x => x.MaterielCode == g.Key.MaterielCode && x.BatchNo == g.Key.BatchNo && x.OrderType == (int)outboundOrder.OrderType && x.OrderNo == outboundOrder.OrderNo)
                })
                .ToList();
            return materielGroups;
        }
        /// <summary>
        /// å¤„理物料的任务生成
        /// </summary>
        /// <param name="outboundOrder">出库订单</param>
        /// <param name="materielCalc">按物料的出库计算结果</param>
        /// <param name="request">分拣出库请求</param>
        /// <param name="factoryArea"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        private (List<PickedStockDetailDTO> PickedDetails, List<Dt_Task> Tasks, List<Dt_OutStockLockInfo> OutStockLockInfo) ProcessMaterielTaskGeneration(Dt_OutboundOrder outboundOrder, MaterielOutboundCalculationDTO materielCalc, PickingOutboundRequestDTO request, string factoryArea)
        {
            List<PickedStockDetailDTO> pickedDetails = new List<PickedStockDetailDTO>();
            List<Dt_Task> generatedTasks = new List<Dt_Task>();
            // æž„建库存查询条件(包含库存表、库存明细)
            List<Dt_StockInfo> stockQuery = BuildStockQueryWithInfo(materielCalc, factoryArea);
            if (!stockQuery.Any())
            {
                throw new Exception($"物料 {materielCalc.MaterielCode} å¯¹åº”的库存不存在");
            }
            // æ‰¹é‡è®¡ç®—总可用库存数量
            (Dictionary<int, decimal> AvailableStockMap, Dictionary<int, List<Dt_OutStockLockInfo>> LockStockMap) data = GetBatchAvailableStockQuantities(materielCalc, stockQuery);
            // å¯ç”¨åº“存数量映射
            Dictionary<int, decimal> availableStockMap = data.AvailableStockMap;
            // ç‰©æ–™æ€»å¯ç”¨åº“存数量
            decimal totalAvailableStock = availableStockMap.Values.Sum();
            // å·²é”å®šåº“存数量映射
            Dictionary<int, List<Dt_OutStockLockInfo>> lockStockMap = data.LockStockMap;
            // éªŒè¯æ€»å¯ç”¨åº“存是否满足出库需求
            if (totalAvailableStock < materielCalc.UnallocatedQuantity)
            {
                throw new Exception($"物料 {materielCalc.MaterielCode} å¯ç”¨åº“å­˜ {totalAvailableStock} ä¸è¶³å‡ºåº“数量 {materielCalc.UnallocatedQuantity}");
            }
            // éœ€åˆ†é…æ•°é‡
            decimal remainingQuantity = materielCalc.UnallocatedQuantity;
            // å·²åˆ†é…çš„æ‰˜ç›˜åˆ—表
            List<string> allocatedPallets = new List<string>();
            // èŽ·å–ç¬¬ä¸€ä¸ªå‡ºåº“æ˜Žç»†
            Dt_OutboundOrderDetail firstDetail = materielCalc.Details.First();
            // é¢„加载库存明细和锁定记录
            List<int> stockIds = stockQuery.Select(x => x.Id).ToList();
            Dictionary<int, List<Dt_StockInfoDetail>> stockDetailMap = stockQuery.ToDictionary(x => x.Id, x => x.Details);
            // è®°å½•每个托盘的实际分配量
            Dictionary<string, decimal> palletAllocations = new Dictionary<string, decimal>();
            List<Dt_OutStockLockInfo> lockInfoList = new List<Dt_OutStockLockInfo>();
            foreach (var stock in stockQuery)
            {
                if (remainingQuantity <= 0) break;
                // å½“前库存可用数量
                decimal availableQuantity = availableStockMap.GetValueOrDefault(stock.Id, 0);
                if (availableQuantity <= 0) continue;
                decimal allocateQuantity = Math.Min(remainingQuantity, availableQuantity);
                (decimal ActualAllocatedQuantity, List<Dt_OutStockLockInfo> LockInfoList) actualAllocated = AllocateStockQuantity(stock, allocateQuantity, availableQuantity, outboundOrder, firstDetail, request, lockStockMap.GetValueOrDefault(stock.Id, new List<Dt_OutStockLockInfo>()), stockDetailMap);
                decimal actualAllocatedQuantity = actualAllocated.ActualAllocatedQuantity;
                if (actualAllocatedQuantity > 0)
                {
                    allocatedPallets.Add(stock.PalletCode);
                    palletAllocations[stock.PalletCode] = actualAllocatedQuantity; // è®°å½•实际分配量
                    remainingQuantity -= actualAllocatedQuantity;
                    lockInfoList.AddRange(actualAllocated.LockInfoList);
                }
            }
            foreach (var palletCode in allocatedPallets)
            {
                Dt_StockInfo stock = stockQuery.First(x => x.PalletCode == palletCode);
                // èŽ·å–å®žé™…åˆ†é…çš„æ•°é‡
                decimal actualAllocatedQuantity = palletAllocations.GetValueOrDefault(palletCode, 0);
                // è®¡ç®—分配后剩余的库存数量
                decimal originalAvailableQuantity = availableStockMap.GetValueOrDefault(stock.Id, 0);
                decimal remainingStockQuantity = Math.Max(0, originalAvailableQuantity - actualAllocatedQuantity);
                pickedDetails.Add(new PickedStockDetailDTO
                {
                    PalletCode = palletCode,
                    MaterielCode = materielCalc.MaterielCode,
                    OutboundQuantity = actualAllocatedQuantity, // æœ¬æ¬¡å®žé™…分配的出库量
                    RemainingQuantity = remainingStockQuantity, // åˆ†é…åŽå‰©ä½™çš„可用库存
                    LocationCode = stock.LocationCode,
                    OutStockLockInfos = lockInfoList
                });
                Dt_OutStockLockInfo? outStockLockInfo = lockInfoList.FirstOrDefault(x => x.PalletCode == palletCode);
                int taskNum = outStockLockInfo?.TaskNum ?? Db.Ado.GetScalar($"SELECT NEXT VALUE FOR SeqTaskNum").ObjToInt();
                Dt_Task task = GenerationOutTask(stock, TaskTypeEnum.Outbound, taskNum, request.OutboundTargetLocation);
                if (generatedTasks.FirstOrDefault(x => x.PalletCode == stock.PalletCode) == null) generatedTasks.Add(task);
            }
            return (pickedDetails, generatedTasks, lockInfoList);
        }
        /// <summary>
        /// ç”Ÿæˆå‡ºåº“任务
        /// </summary>
        /// <param name="stockInfo"></param>
        /// <param name="taskType"></param>
        /// <param name="outStation"></param>
        /// <returns></returns>
        public Dt_Task GenerationOutTask(Dt_StockInfo stockInfo, TaskTypeEnum taskType, int taskNum, string outStation)
        {
            Dt_Task task = new()
            {
                CurrentAddress = stockInfo.LocationCode,
                Grade = 0,
                PalletCode = stockInfo.PalletCode,
                NextAddress = outStation,
                Roadway = "",
                SourceAddress = stockInfo.LocationCode,
                TargetAddress = outStation,
                TaskStatus = TaskStatusEnum.New.ObjToInt(),
                TaskType = taskType.ObjToInt(),
                TaskNum = taskNum,
                PalletType = stockInfo.PalletType,
                WarehouseId = stockInfo.WarehouseId,
            };
            return task;
        }
        /// <summary>
        /// æž„建库存查询条件(包含库存信息和库存明细)
        /// </summary>
        /// <param name="materielCalc"></param>
        /// <param name="factoryArea"></param>
        /// <returns></returns>
        private List<Dt_StockInfo> BuildStockQueryWithInfo(MaterielOutboundCalculationDTO materielCalc, string factoryArea)
        {
            // åŸºç¡€æŸ¥è¯¢æ¡ä»¶ï¼šç‰©æ–™ç¼–号、批次号(如果提供)、库存数量>0
            ISugarQueryable<Dt_StockInfoDetail> stockDetails = _stockDetailService.Repository.Db.Queryable<Dt_StockInfoDetail>().Where(x => x.MaterielCode == materielCalc.MaterielCode && x.StockQuantity > 0);
            // æ ¹æ®æ¡ä»¶æ·»åŠ ä¾›åº”å•†ç¼–å·åŒ¹é…ï¼ˆä¸ä¸ºç©ºæ—¶æ‰éœ€è¦åŒ¹é…ï¼‰
            if (!string.IsNullOrEmpty(materielCalc.SupplyCode))
            {
                stockDetails = stockDetails.Where(x => x.SupplyCode == materielCalc.SupplyCode);
            }
            // æ ¹æ®æ¡ä»¶æ·»åŠ ä»“åº“ç¼–å·åŒ¹é…ï¼ˆä¸ä¸ºç©ºæ—¶æ‰éœ€è¦åŒ¹é…ï¼‰
            if (!string.IsNullOrEmpty(materielCalc.WarehouseCode))
            {
                stockDetails = stockDetails.Where(x => x.WarehouseCode == materielCalc.WarehouseCode);
            }
            // æ ¹æ®æ¡ä»¶æ·»åŠ åŽ‚åŒºåŒ¹é…ï¼ˆä¸ä¸ºç©ºæ—¶æ‰éœ€è¦åŒ¹é…ï¼‰
            if (!string.IsNullOrEmpty(factoryArea))
            {
                stockDetails = stockDetails.Where(x => x.FactoryArea == factoryArea);
            }
            // æ ¹æ®æ‰¹æ¬¡å·è¿›è¡Œè¿‡æ»¤ï¼ˆå¦‚果提供)
            if (!string.IsNullOrEmpty(materielCalc.BatchNo))
            {
                stockDetails = stockDetails.Where(x => x.BatchNo == materielCalc.BatchNo);
            }
            List<Dt_StockInfoDetail> stockDetailList = stockDetails.ToList();
            // èŽ·å–å¯ç”¨è´§ä½ç¼–å·
            List<string> locationCodes = _locationInfoService.Repository.QueryData(x => (x.LocationStatus == LocationStatusEnum.InStock.ObjToInt() /*|| x.LocationStatus == LocationStatusEnum.Lock.ObjToInt()*/) && x.EnableStatus == EnableStatusEnum.Normal.ObjToInt()).Select(x => x.LocationCode).ToList();
            // èŽ·å–æ‰€æœ‰ç›¸å…³çš„åº“å­˜ä¿¡æ¯
            List<int> stockIds = stockDetailList.GroupBy(x => x.StockId).Select(x => x.Key).ToList();
            List<Dt_StockInfo> stockInfos = _stockInfoService.Repository.QueryData(x => stockIds.Contains(x.Id) && (x.StockStatus == StockStatusEmun.入库完成.ObjToInt() /*|| x.StockStatus == StockStatusEmun.出库锁定.ObjToInt()*/) && !string.IsNullOrEmpty(x.LocationCode) && locationCodes.Contains(x.LocationCode));
            // åœ¨å†…存中关联数据
            foreach (var stockInfo in stockInfos)
            {
                stockInfo.Details = new List<Dt_StockInfoDetail>();
                stockInfo.Details = stockDetailList.Where(x => x.StockId == stockInfo.Id).ToList();
            }
            return stockInfos;
        }
        /// <summary>
        /// æ‰¹é‡èŽ·å–æ‰˜ç›˜å¯ç”¨åº“å­˜ä¿¡æ¯
        /// </summary>
        /// <param name="stockInfos">库存信息列表</param>
        /// <param name="materielCode">物料编号</param>
        /// <returns>返回值为(库存主键,可用数量)字典</returns>
        private (Dictionary<int, decimal> AvailableStockMap, Dictionary<int, List<Dt_OutStockLockInfo>> LockStockMap) GetBatchAvailableStockQuantities(MaterielOutboundCalculationDTO materielCalc, List<Dt_StockInfo> stockInfos)
        {
            List<int> stockIds = stockInfos.Select(x => x.Id).ToList();
            Dictionary<int, decimal> availableStockMap = new Dictionary<int, decimal>(); // å¯ç”¨åº“存数量
            Dictionary<int, List<Dt_OutStockLockInfo>> lockStockMap = new Dictionary<int, List<Dt_OutStockLockInfo>>(); // å·²é”å®šåº“存数量
            // æ‰¹é‡æŸ¥è¯¢å·²åˆ†é…æ•°é‡
            List<Dt_OutStockLockInfo> allocatedData = materielCalc.OutStockLockInfos.Where(x => stockIds.Contains(x.StockId) && x.MaterielCode == materielCalc.MaterielCode).ToList();
            foreach (var stockInfo in stockInfos)
            {
                // è®¡ç®—总库存数量
                decimal totalQuantity = stockInfo.Details.Sum(x => x.StockQuantity);
                List<Dt_OutStockLockInfo> outStockLockInfos = allocatedData.Where(x => x.StockId == stockInfo.Id).ToList();
                // è®¡ç®—已分配数量
                decimal allocatedQuantity = outStockLockInfos.Sum(x => x.AllocatedQuantity);
                availableStockMap[stockInfo.Id] = Math.Max(0, totalQuantity - allocatedQuantity);
                lockStockMap[stockInfo.Id] = outStockLockInfos;
            }
            return (availableStockMap, lockStockMap);
        }
        /// <summary>
        /// åˆ†é…åº“å­˜
        /// </summary>
        /// <param name="stockInfo">库存信息</param>
        /// <param name="allocateQuantity">要分配的数量</param>
        /// <param name="availableQuantity">可分配的数量</param>
        /// <param name="outboundOrder">出库单</param>
        /// <param name="detail">出库单明细</param>
        /// <param name="request"></param>
        /// <param name="lockInfos"></param>
        /// <param name="stockDetailMap"></param>
        /// <returns></returns>
        private (decimal ActualAllocatedQuantity, List<Dt_OutStockLockInfo> LockInfoList) AllocateStockQuantity(Dt_StockInfo stockInfo, decimal allocateQuantity, decimal availableQuantity, Dt_OutboundOrder outboundOrder, Dt_OutboundOrderDetail detail, PickingOutboundRequestDTO request, List<Dt_OutStockLockInfo> lockInfos, Dictionary<int, List<Dt_StockInfoDetail>> stockDetailMap = null)
        {
            decimal actualAllocatedQuantity = Math.Min(allocateQuantity, availableQuantity); // å®žé™…分配数量
            List<Dt_OutStockLockInfo> lockInfoList = new List<Dt_OutStockLockInfo>();
            if (actualAllocatedQuantity > 0)
            {
                //检查目标位置一致性:如果托盘已有锁定记录且目标位置不同,则不允许分配
                if (lockInfos.Any() && !string.IsNullOrEmpty(lockInfos.First().OutboundTargetLocation))
                {
                    if (!string.Equals(lockInfos.First().OutboundTargetLocation, request.OutboundTargetLocation, StringComparison.OrdinalIgnoreCase))
                    {
                        // æ‰˜ç›˜çš„目标位置与新请求的目标位置不同,不允许使用该托盘
                        return (0, lockInfoList);
                    }
                }
                Dt_OutStockLockInfo? lockInfo = lockInfos.FirstOrDefault(x => x.StockId == stockInfo.Id && x.Status == OutLockStockStatusEnum.已分配.ObjToInt() && x.PalletCode == stockInfo.PalletCode && x.OrderNo == outboundOrder.OrderNo);
                if (lockInfo != null)
                {
                    // è¿½åŠ å½“å‰æ˜Žç»†ID到OrderDetailIds字段(避免重复)
                    List<string> currentIds = lockInfo.OrderDetailIds?.Split(',').ToList() ?? new List<string>();
                    if (!currentIds.Contains(detail.Id.ToString()))
                    {
                        currentIds.Add(detail.Id.ToString());
                        lockInfo.OrderDetailIds = string.Join(",", currentIds);
                    }
                    // è®¡ç®—该托盘该物料的总累计已出库数量(跨所有单据)
                    decimal totalAllocatedQuantity = CalcTotalAllocatedQuantity(lockInfos, stockInfo.Id, detail.MaterielCode);
                    // æ›´æ–°åˆ†é…å‡ºåº“量
                    decimal beforeAssignQuantity = totalAllocatedQuantity; // æœ¬æ¬¡åˆ†é…å‰çš„æ€»ç´¯è®¡é‡
                    lockInfo.AssignQuantity += actualAllocatedQuantity; // ç´¯åŠ æœ¬æ¬¡åˆ†é…æ•°é‡
                    lockInfo.AllocatedQuantity = beforeAssignQuantity; // è®°å½•本次分配前的总累计量
                    lockInfoList.Add(lockInfo);
                }
                else
                {
                    // åˆ›å»ºæ–°çš„锁定记录(使用预加载的库存明细)
                    decimal originalQuantity = 0;
                    if (stockDetailMap?.ContainsKey(stockInfo.Id) == true)
                    {
                        originalQuantity = stockDetailMap[stockInfo.Id].Sum(x => x.StockQuantity);
                    }
                    // èŽ·å–è¯¥ç‰©æ–™åœ¨è¯¥è®¢å•ä¸­çš„æ‰€æœ‰æ˜Žç»†ID
                    List<string> allDetailIds = (outboundOrder.Details.Where(x =>
                        x.OrderId == outboundOrder.Id &&
                        x.MaterielCode == detail.MaterielCode &&
                        x.BatchNo == detail.BatchNo &&
                        x.SupplyCode == detail.SupplyCode &&
                        x.WarehouseCode == detail.WarehouseCode))
                        .Select(x => x.Id.ToString())
                        .ToList();
                    // è®¡ç®—该托盘该物料的总累计已出库数量(跨所有单据)
                    decimal totalAllocatedQuantity = CalcTotalAllocatedQuantity(lockInfos, stockInfo.Id, detail.MaterielCode);
                    lockInfo = new Dt_OutStockLockInfo
                    {
                        OrderNo = request.OrderNo,
                        OrderDetailIds = string.Join(",", allDetailIds), // è®°å½•该物料的所有明细ID
                        OrderType = outboundOrder.OrderType,
                        BatchNo = detail.BatchNo,
                        MaterielCode = detail.MaterielCode,
                        MaterielName = detail.MaterielName,
                        StockId = stockInfo.Id,
                        OrderQuantity = allDetailIds.SelectMany(id => allDetailIds).Count() > 1
                            ? (outboundOrder.Details.Where(x =>
                                x.OrderId == outboundOrder.Id &&
                                x.MaterielCode == detail.MaterielCode &&
                                x.BatchNo == detail.BatchNo &&
                                x.SupplyCode == detail.SupplyCode &&
                                x.WarehouseCode == detail.WarehouseCode))
                                .Sum(x => x.OrderQuantity)
                            : detail.OrderQuantity, // å¦‚果只有一个明细,使用明细数量
                        OriginalQuantity = originalQuantity,
                        AssignQuantity = actualAllocatedQuantity, // æœ¬æ¬¡åˆ†é…æ•°é‡
                        AllocatedQuantity = totalAllocatedQuantity, // æœ¬æ¬¡åˆ†é…å‰çš„æ€»ç´¯è®¡å·²å‡ºåº“数量(跨所有单据)
                        LocationCode = stockInfo.LocationCode,
                        PalletCode = stockInfo.PalletCode,
                        Unit = detail.Unit,
                        OutboundTargetLocation = request.OutboundTargetLocation,
                        Status = OutLockStockStatusEnum.已分配.ObjToInt(),
                        SupplyCode = detail.SupplyCode,
                        WarehouseCode = detail.WarehouseCode,
                        FactoryArea = outboundOrder.FactoryArea,
                        TaskNum = Db.Ado.GetScalar($"SELECT NEXT VALUE FOR SeqTaskNum").ObjToInt(),
                        OrderDetailId = 0 // æœªå…³è”具体明细ID
                    };
                    lockInfoList.Add(lockInfo);
                }
            }
            return (actualAllocatedQuantity, lockInfoList);
        }
        /// <summary>
        /// è®¡ç®—该托盘累计已分配数量
        /// </summary>
        /// <param name="lockInfos"></param>
        /// <param name="stockId"></param>
        /// <param name="materielCode"></param>
        /// <returns></returns>
        private decimal CalcTotalAllocatedQuantity(List<Dt_OutStockLockInfo> lockInfos, int stockId, string materielCode)
        {
            // æŸ¥è¯¢è¯¥æ‰˜ç›˜è¯¥ç‰©æ–™åœ¨æ‰€æœ‰é”å®šè®°å½•中的最大已分配数量
            List<Dt_OutStockLockInfo> lockRecords = _outboundLockInfoService.Repository.QueryData(x =>
                x.StockId == stockId &&
                x.MaterielCode == materielCode);
            if (lockRecords == null || !lockRecords.Any())
            {
                return 0;
            }
            // è¿”回累计已分配数量
            return lockRecords.Sum(x => x.AssignQuantity);
        }
        /// <summary>
        /// æ›´æ–°å‡ºåº“单状态
        /// </summary>
        public bool UpdateOutboundOrderStatus(string orderNo, int status)
        {
            try
            {
                Dt_OutboundOrder outboundOrder = _outboundOrderService.Repository.QueryFirst(x => x.OrderNo == orderNo);
                if (outboundOrder == null) return false;
                outboundOrder.OrderStatus = status;
                _outboundOrderService.Repository.UpdateData(outboundOrder);
                return true;
            }
            catch
            {
                return false;
            }
        }
        /// <summary>
        ///
        /// </summary>
        /// <param name="palletCodes"></param>
        /// <param name="status"></param>
        /// <returns></returns>
        public bool UpdateStockStatus(List<string> palletCodes, int status)
        {
            try
            {
                List<Dt_StockInfo> stockInfos = _stockInfoService.Repository.QueryData(x => palletCodes.Contains(x.PalletCode));
                stockInfos.ForEach(stockInfo =>
                {
                    stockInfo.StockStatus = status;
                });
                _stockInfoService.Repository.UpdateData(stockInfos);
                return true;
            }
            catch
            {
                return false;
            }
        }
        /// <summary>
        ///
        /// </summary>
        /// <param name="locationCodes"></param>
        /// <param name="status"></param>
        /// <returns></returns>
        public bool UpdateLocationStatus(List<string> locationCodes, int status)
        {
            try
            {
                List<Dt_LocationInfo> locationInfos = _locationInfoService.Repository.QueryData(x => locationCodes.Contains(x.LocationCode));
                locationInfos.ForEach(x =>
                {
                    x.LocationStatus = status;
                });
                _locationInfoService.Repository.UpdateData(locationInfos);
                return true;
            }
            catch
            {
                return false;
            }
        }
        /// <summary>
        ///
        /// </summary>
        /// <param name="outStockLockInfos"></param>
        /// <returns></returns>
        public bool UpdateOutStockLockInfo(List<Dt_OutStockLockInfo> outStockLockInfos)
        {
            try
            {
                List<Dt_OutStockLockInfo> updateData = outStockLockInfos.Where(x => x.Id > 0).ToList();
                _outboundLockInfoService.Repository.UpdateData(updateData);
                List<Dt_OutStockLockInfo> addData = outStockLockInfos.Where(x => x.Id <= 0).ToList();
                _outboundLockInfoService.Repository.AddData(addData);
                return true;
            }
            catch
            {
                return false;
            }
        }
        /// <summary>
        /// å‡ºåº“完成处理(扫描条码扣减库存)
        /// </summary>
        /// <param name="request">出库完成请求</param>
        /// <returns>出库完成响应</returns>
        public WebResponseContent CompleteOutboundWithBarcode(OutboundCompleteRequestDTO request)
        {
            WebResponseContent content = WebResponseContent.Instance;
            OutboundCompleteResponseDTO response = new();
            try
            {
                // 1. æ ¹æ®æ‰˜ç›˜å·æŸ¥æ‰¾åº“存信息
                Dt_StockInfo stockInfo = _stockInfoService.Repository.QueryFirst(x => x.PalletCode == request.PalletCode);
                if (stockInfo == null)
                {
                    response.Success = false;
                    response.Message = $"托盘号 {request.PalletCode} å¯¹åº”的库存不存在";
                    return WebResponseContent.Instance.Error($"托盘号 {request.PalletCode} å¯¹åº”的库存不存在");
                }
                // 2. æ ¹æ®æ¡ç æŸ¥æ‰¾åº“存明细
                Dt_StockInfoDetail stockDetail = _stockDetailService.Repository.QueryFirst(x => x.Barcode == request.Barcode);
                if (stockDetail == null)
                {
                    response.Success = false;
                    response.Message = $"条码 {request.Barcode} å¯¹åº”的库存明细不存在";
                    return WebResponseContent.Instance.Error($"条码 {request.Barcode} å¯¹åº”的库存明细不存在");
                }
                // 3. éªŒè¯åº“存明细与托盘是否匹配
                if (stockDetail.StockId != stockInfo.Id)
                {
                    response.Success = false;
                    response.Message = $"条码 {request.Barcode} ä¸å±žäºŽæ‰˜ç›˜å· {request.PalletCode} çš„库存明细";
                    return WebResponseContent.Instance.Error($"条码 {request.Barcode} ä¸å±žäºŽæ‰˜ç›˜å· {request.PalletCode} çš„库存明细");
                }
                // 4. æŸ¥æ‰¾å‡ºåº“单信息
                Dt_OutboundOrder outboundOrder = _outboundOrderService.Repository.QueryFirst(o => o.OrderNo == request.OrderNo);
                if (outboundOrder == null)
                {
                    response.Success = false;
                    response.Message = $"出库单 {request.OrderNo} ä¸å­˜åœ¨";
                    return WebResponseContent.Instance.Error($"出库单 {request.OrderNo} ä¸å­˜åœ¨");
                }
                // 5. æŸ¥æ‰¾å‡ºåº“单明细信息
                List<Dt_OutboundOrderDetail> outboundOrderDetails = FindMatchingOutboundDetails(outboundOrder.Id, stockDetail);
                if (!outboundOrderDetails.Any())
                {
                    response.Success = false;
                    response.Message = $"未找到匹配的出库单明细,物料:{stockDetail.MaterielCode},批次:{stockDetail.BatchNo}";
                    return WebResponseContent.Instance.Error($"未找到匹配的出库单明细,物料:{stockDetail.MaterielCode},批次:{stockDetail.BatchNo}");
                }
                // 6. æŸ¥æ‰¾é”å®šè®°å½•
                Dt_OutStockLockInfo lockInfo = _outboundLockInfoService.Repository.QueryFirst(x =>
                    x.OrderNo == request.OrderNo &&
                    x.StockId == stockInfo.Id &&
                    x.MaterielCode == stockDetail.MaterielCode &&
                    x.PalletCode == stockInfo.PalletCode);
                if (lockInfo == null || lockInfo.AssignQuantity <= 0)
                {
                    response.Success = false;
                    response.Message = $"该库存没有分配出库量,条码:{request.Barcode}";
                    return WebResponseContent.Instance.Error($"该库存没有分配出库量,条码:{request.Barcode}");
                }
                // 7. è®¡ç®—实际出库量
                decimal actualOutboundQuantity = CalculateActualOutboundQuantity(stockDetail, outboundOrderDetails, lockInfo);// éœ€å‡ºåº“量
                if (actualOutboundQuantity <= 0)
                {
                    decimal totalAllocatedQuantity = lockInfo.AllocatedQuantity;
                    decimal availableOutboundQuantity = lockInfo.AssignQuantity - totalAllocatedQuantity;
                    decimal detailRemainingQuantity = outboundOrderDetails.Sum(x => x.OrderQuantity - x.OverOutQuantity - x.MoveQty);
                    response.Success = false;
                    response.Message = $"无法出库,条码:{request.Barcode},库存:{stockDetail.StockQuantity},已出库:{totalAllocatedQuantity},分配量:{lockInfo.AssignQuantity},明细剩余:{detailRemainingQuantity}";
                    return WebResponseContent.Instance.Error($"无法出库,条码:{request.Barcode},库存:{stockDetail.StockQuantity},已出库:{totalAllocatedQuantity},分配量:{lockInfo.AssignQuantity},明细剩余:{detailRemainingQuantity}");
                }
                // 8. åˆ¤æ–­æ˜¯å¦éœ€è¦æ‹†åŒ…(当出库数量小于库存数量时需要拆包)
                bool isUnpacked = actualOutboundQuantity < stockDetail.StockQuantity;
                string newBarcode = string.Empty;
                // 9. å¼€å¯äº‹åŠ¡
                _unitOfWorkManage.BeginTran();
                try
                {
                    decimal beforeQuantity = stockDetail.StockQuantity; // åŽŸå§‹åº“å­˜é‡
                    // æ ¹æ®æ˜¯å¦æ‹†åŒ…执行不同的操作
                    if (isUnpacked)
                    {
                        newBarcode = PerformUnpackOperation(stockDetail, stockInfo, actualOutboundQuantity, request, beforeQuantity, lockInfo.TaskNum.GetValueOrDefault());
                    }
                    else
                    {
                        PerformFullOutboundOperation(stockDetail, stockInfo, actualOutboundQuantity, request, beforeQuantity, lockInfo.TaskNum.GetValueOrDefault());
                    }
                    decimal allocatedQuantity = actualOutboundQuantity;
                    List<Dt_OutboundOrderDetail> updateDetails = new();
                    foreach (var item in outboundOrderDetails)
                    {
                        if (allocatedQuantity <= 0) break;
                        if (item.OrderQuantity - item.MoveQty - item.OverOutQuantity >= allocatedQuantity)
                        {
                            item.OverOutQuantity += allocatedQuantity;
                            allocatedQuantity = 0;
                        }
                        else
                        {
                            allocatedQuantity -= (item.OrderQuantity - item.MoveQty - item.OverOutQuantity);
                            item.OverOutQuantity = item.OrderQuantity - item.MoveQty;
                        }
                        updateDetails.Add(item);
                    }
                    lockInfo.SortedQuantity = lockInfo.SortedQuantity + actualOutboundQuantity;
                    // æ›´æ–°é”å®šè®°å½•
                    _outboundLockInfoService.Repository.UpdateData(lockInfo);
                    // æ›´æ–°å‡ºåº“单明细的已出库数量
                    _detailService.Repository.UpdateData(updateDetails);
                    // æ›´æ–°é”å®šè®°å½•的累计已出库数量(需要更新该托盘该物料的所有相关记录)
                    UpdateLockInfoAllocatedQuantity(stockInfo.Id, stockDetail.MaterielCode, stockDetail.BatchNo, actualOutboundQuantity);
                    // æäº¤äº‹åŠ¡
                    _unitOfWorkManage.CommitTran();
                    // æž„建返回信息
                    ScannedStockDetailDTO scannedDetail = new ScannedStockDetailDTO
                    {
                        StockDetailId = stockDetail.Id,
                        PalletCode = stockInfo.PalletCode,
                        MaterielCode = stockDetail.MaterielCode,
                        MaterielName = stockDetail.MaterielName,
                        BatchNo = stockDetail.BatchNo,
                        OriginalBarcode = request.Barcode,
                        BeforeQuantity = beforeQuantity,
                        AfterQuantity = isUnpacked ? actualOutboundQuantity : 0,
                        ChangeQuantity = -actualOutboundQuantity,
                        IsUnpacked = isUnpacked
                    };
                    response.Success = true;
                    response.Message = isUnpacked ? $"拆包出库完成,已生成新条码:{newBarcode}" : "出库完成";
                    response.ScannedDetail = scannedDetail;
                    response.UpdatedDetails = updateDetails;
                    response.NewBarcode = newBarcode;
                    // æ£€æŸ¥å‡ºåº“单是否完成
                    if (CheckOutboundOrderCompleted(request.OrderNo))
                    {
                        UpdateOutboundOrderStatus(request.OrderNo, OutOrderStatusEnum.出库完成.ObjToInt());
                    }
                }
                catch (Exception ex)
                {
                    _unitOfWorkManage.RollbackTran();
                    response.Success = false;
                    response.Message = $"出库处理失败:{ex.Message}";
                    return WebResponseContent.Instance.Error(ex.Message);
                }
                content = WebResponseContent.Instance.OK(data: response);
            }
            catch (Exception ex)
            {
                content = WebResponseContent.Instance.Error("处理出库完成失败:" + ex.Message);
            }
            return content;
        }
        /// <summary>
        ///
        /// </summary>
        /// <param name="orderId"></param>
        /// <param name="stockDetail"></param>
        /// <returns></returns>
        private List<Dt_OutboundOrderDetail> FindMatchingOutboundDetails(int orderId, Dt_StockInfoDetail stockDetail)
        {
            List<Dt_OutboundOrderDetail> details = _detailService.Repository.QueryData(x =>
                x.OrderId == orderId &&
                x.MaterielCode == stockDetail.MaterielCode && x.OrderQuantity - x.MoveQty > x.OverOutQuantity);
            // ç²¾ç¡®åŒ¹é…ï¼šå¤„理null值的批次、供应商、仓库
            List<Dt_OutboundOrderDetail> exactMatches = details.Where(x =>
                (string.IsNullOrEmpty(x.BatchNo)) || x.BatchNo == stockDetail.BatchNo
            ).Where(x =>
                (string.IsNullOrEmpty(x.SupplyCode)) || x.SupplyCode == stockDetail.SupplyCode
            ).Where(x =>
                (string.IsNullOrEmpty(x.WarehouseCode)) || x.WarehouseCode == stockDetail.WarehouseCode
            ).ToList();
            return exactMatches;
        }
        /// <summary>
        /// è®¡ç®—实际出库数量
        /// </summary>
        private decimal CalculateActualOutboundQuantity(Dt_StockInfoDetail stockDetail, List<Dt_OutboundOrderDetail> outboundDetails, Dt_OutStockLockInfo lockInfo)
        {
            decimal availableOutboundQuantity = lockInfo.AssignQuantity;
            decimal detailRemainingQuantity = outboundDetails.Sum(x => x.OrderQuantity - x.OverOutQuantity - x.MoveQty);//outboundDetail.OrderQuantity - outboundDetail.OverOutQuantity;
            return Math.Min(
                Math.Min(availableOutboundQuantity, detailRemainingQuantity),
                stockDetail.StockQuantity);
        }
        /// <summary>
        /// æ‰§è¡Œæ‹†åŒ…操作
        /// </summary>
        /// <param name="stockDetail"></param>
        /// <param name="stockInfo"></param>
        /// <param name="actualOutboundQuantity"></param>
        /// <param name="request"></param>
        /// <param name="beforeQuantity"></param>
        /// <param name="taskNum"></param>
        /// <returns></returns>
        private string PerformUnpackOperation(Dt_StockInfoDetail stockDetail, Dt_StockInfo stockInfo,
            decimal actualOutboundQuantity, OutboundCompleteRequestDTO request, decimal beforeQuantity, int taskNum)
        {
            string newBarcode = GenerateNewBarcode();
            // ä¿å­˜åŽŸå§‹åº“å­˜æ˜Žç»†åˆ°åŽ†å²è®°å½•
            Dt_StockInfoDetail_Hty originalHistoryRecord = new Dt_StockInfoDetail_Hty
            {
                SourceId = stockDetail.Id,
                OperateType = "拆包-原始记录",
                InsertTime = DateTime.Now,
                StockId = stockDetail.StockId,
                MaterielCode = stockDetail.MaterielCode,
                MaterielName = stockDetail.MaterielName,
                OrderNo = stockDetail.OrderNo,
                BatchNo = stockDetail.BatchNo,
                ProductionDate = stockDetail.ProductionDate,
                EffectiveDate = stockDetail.EffectiveDate,
                SerialNumber = stockDetail.SerialNumber,
                StockQuantity = stockDetail.StockQuantity,
                OutboundQuantity = stockDetail.OutboundQuantity,
                Status = stockDetail.Status,
                Unit = stockDetail.Unit,
                InboundOrderRowNo = stockDetail.InboundOrderRowNo,
                SupplyCode = stockDetail.SupplyCode,
                FactoryArea = stockDetail.FactoryArea,
                WarehouseCode = stockDetail.WarehouseCode,
                Remark = $"拆包前原始记录,原条码:{request.Barcode},原数量:{stockDetail.StockQuantity},出库数量:{actualOutboundQuantity},操作者:{request.Operator}"
            };
            _stockDetailHistoryService.Repository.AddData(originalHistoryRecord);
            // ä¿å­˜å‰©ä½™éƒ¨åˆ†åˆ°åŽ†å²è®°å½•
            decimal remainingQuantity = stockDetail.StockQuantity - actualOutboundQuantity;
            if (remainingQuantity > 0)
            {
                // æ›´æ–°åŽŸåº“å­˜æ˜Žç»†
                stockDetail.StockQuantity = remainingQuantity;
                stockDetail.Remark = $"拆包后更新,原条码:{request.Barcode},新数量:{remainingQuantity},操作者:{request.Operator}";
                _stockDetailService.Repository.UpdateData(stockDetail);
            }
            // è®°å½•拆包变动
            Dt_StockQuantityChangeRecord unpackChangeRecord = new Dt_StockQuantityChangeRecord
            {
                StockDetailId = stockDetail.Id,
                PalleCode = stockInfo.PalletCode,
                MaterielCode = stockDetail.MaterielCode,
                MaterielName = stockDetail.MaterielName,
                BatchNo = stockDetail.BatchNo,
                OriginalSerilNumber = request.Barcode,
                NewSerilNumber = newBarcode,
                OrderNo = request.OrderNo,
                TaskNum = taskNum,
                ChangeType = (int)StockChangeTypeEnum.Outbound,
                ChangeQuantity = -actualOutboundQuantity,
                BeforeQuantity = beforeQuantity,
                AfterQuantity = beforeQuantity - actualOutboundQuantity,
                SupplyCode = stockDetail.SupplyCode,
                WarehouseCode = stockDetail.WarehouseCode,
                Remark = $"拆包出库,原条码:{request.Barcode},新条码:{newBarcode},出库数量:{actualOutboundQuantity},剩余:{remainingQuantity},操作者:{request.Operator}"
            };
            _stockChangeService.Repository.AddData(unpackChangeRecord);
            return newBarcode;
        }
        /// <summary>
        /// æ‰§è¡Œå®Œæ•´å‡ºåº“操作(不拆包)
        /// </summary>
        private void PerformFullOutboundOperation(Dt_StockInfoDetail stockDetail, Dt_StockInfo stockInfo,
            decimal actualOutboundQuantity, OutboundCompleteRequestDTO request, decimal beforeQuantity, int taskNum)
        {
            // ä¿å­˜åº“存明细到历史记录
            Dt_StockInfoDetail_Hty historyRecord = new Dt_StockInfoDetail_Hty
            {
                SourceId = stockDetail.Id,
                OperateType = "出库完成",
                InsertTime = DateTime.Now,
                StockId = stockDetail.StockId,
                MaterielCode = stockDetail.MaterielCode,
                MaterielName = stockDetail.MaterielName,
                OrderNo = stockDetail.OrderNo,
                BatchNo = stockDetail.BatchNo,
                ProductionDate = stockDetail.ProductionDate,
                EffectiveDate = stockDetail.EffectiveDate,
                SerialNumber = stockDetail.SerialNumber,
                StockQuantity = stockDetail.StockQuantity,
                OutboundQuantity = stockDetail.OutboundQuantity + actualOutboundQuantity,
                Status = stockDetail.Status,
                Unit = stockDetail.Unit,
                InboundOrderRowNo = stockDetail.InboundOrderRowNo,
                SupplyCode = stockDetail.SupplyCode,
                FactoryArea = stockDetail.FactoryArea,
                WarehouseCode = stockDetail.WarehouseCode,
                Remark = $"出库完成删除,条码:{request.Barcode},原数量:{stockDetail.StockQuantity},出库数量:{actualOutboundQuantity},操作者:{request.Operator}"
            };
            _stockDetailHistoryService.Repository.AddData(historyRecord);
            // åˆ é™¤åº“存明细记录
            _stockDetailService.Repository.DeleteData(stockDetail);
            // è®°å½•库存变动
            Dt_StockQuantityChangeRecord changeRecord = new Dt_StockQuantityChangeRecord
            {
                StockDetailId = stockDetail.Id,
                PalleCode = stockInfo.PalletCode,
                MaterielCode = stockDetail.MaterielCode,
                MaterielName = stockDetail.MaterielName,
                BatchNo = stockDetail.BatchNo,
                OriginalSerilNumber = request.Barcode,
                NewSerilNumber = "",
                OrderNo = request.OrderNo,
                TaskNum = taskNum,
                ChangeType = (int)StockChangeTypeEnum.Outbound,
                ChangeQuantity = -actualOutboundQuantity,
                BeforeQuantity = beforeQuantity,
                AfterQuantity = 0,
                SupplyCode = stockDetail.SupplyCode,
                WarehouseCode = stockDetail.WarehouseCode,
                Remark = $"出库完成删除库存明细,条码:{request.Barcode},出库数量:{actualOutboundQuantity},操作者:{request.Operator}"
            };
            _stockChangeService.Repository.AddData(changeRecord);
        }
        /// <summary>
        /// ç”Ÿæˆæ–°çš„æ¡ç 
        /// </summary>
        /// <returns>新条码</returns>
        private string GenerateNewBarcode()
        {
            // ä½¿ç”¨æ—¶é—´æˆ³å’Œéšæœºæ•°ç”Ÿæˆå”¯ä¸€æ¡ç 
            string newBarcode = string.Empty;
            //todo é‡æ–°ç”Ÿæˆæ¡ç é€»è¾‘
            return newBarcode;
        }
        /// <summary>
        /// æ›´æ–°è¯¥æ‰˜ç›˜è¯¥ç‰©æ–™çš„æ‰€æœ‰é”å®šè®°å½•的累计已出库数量
        /// </summary>
        /// <param name="stockId">库存ID</param>
        /// <param name="materielCode">物料编号</param>
        /// <param name="batchNo">批次号</param>
        /// <param name="actualOutboundQuantity">本次实际出库数量</param>
        /// <returns></returns>
        private void UpdateLockInfoAllocatedQuantity(int stockId, string materielCode, string batchNo, decimal actualOutboundQuantity)
        {
            // æŸ¥è¯¢è¯¥æ‰˜ç›˜è¯¥ç‰©æ–™çš„æ‰€æœ‰é”å®šè®°å½•
            List<Dt_OutStockLockInfo> lockRecords = _outboundLockInfoService.Repository.QueryData(x =>
                x.StockId == stockId &&
                x.MaterielCode == materielCode &&
                x.BatchNo == batchNo);
            if (lockRecords != null && lockRecords.Any())
            {
                // æ›´æ–°æ‰€æœ‰ç›¸å…³è®°å½•çš„AllocatedQuantity
                foreach (var record in lockRecords)
                {
                    record.AllocatedQuantity += actualOutboundQuantity;
                }
                // æ‰¹é‡æ›´æ–°
                _outboundLockInfoService.Repository.UpdateData(lockRecords);
            }
        }
        /// <summary>
        /// æ£€æŸ¥å‡ºåº“单是否完成
        /// </summary>
        public bool CheckOutboundOrderCompleted(string orderNo)
        {
            Dt_OutboundOrder outboundOrder = _outboundOrderService.Repository.QueryFirst(x => x.OrderNo == orderNo);
            if (outboundOrder == null) return false;
            List<Dt_OutboundOrderDetail> details = _detailService.Repository.QueryData(x => x.OrderId == outboundOrder.Id);
            // æ£€æŸ¥æ‰€æœ‰æ˜Žç»†çš„已出数量是否都等于单据数量
            return details.All(x => x.OverOutQuantity >= x.OrderQuantity);
        }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Outbound/OutboundController.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,73 @@

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using WIDESEA_Core;
using WIDESEA_DTO.CalcOut;
namespace WIDESEA_WMSServer.Controllers.Outbound
{
    [Route("api/[controller]")]
    [ApiController]
    public class OutboundController : ControllerBase
    {
        private readonly WIDESEA_IOutboundService.IOutboundService _outboundService;
        public OutboundController(WIDESEA_IOutboundService.IOutboundService outboundService)
        {
            _outboundService = outboundService;
        }
        /// <summary>
        /// åˆ†æ‹£å‡ºåº“操作
        /// </summary>
        /// <param name="request">分拣出库请求</param>
        /// <returns>分拣出库响应</returns>
        [HttpPost, Route("ProcessPickingOutbound"), AllowAnonymous]
        public WebResponseContent ProcessPickingOutbound([FromBody] PickingOutboundRequestDTO request)
        {
            try
            {
                if (!ModelState.IsValid)
                    return WebResponseContent.Instance.Error(string.Join("; ", ModelState.Values
                        .SelectMany(v => v.Errors)
                        .Select(e => e.ErrorMessage)));
                return _outboundService.ProcessPickingOutbound(request);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"分拣出库操作失败: {ex.Message}");
            }
        }
        [HttpPost, Route("CompleteOutboundWithBarcode"), AllowAnonymous]
        public WebResponseContent CompleteOutboundWithBarcode([FromBody] OutboundCompleteRequestDTO request)
        {
            try
            {
                if (!ModelState.IsValid)
                    return WebResponseContent.Instance.Error(string.Join("; ", ModelState.Values
                        .SelectMany(v => v.Errors)
                        .Select(e => e.ErrorMessage)));
                return _outboundService.CompleteOutboundWithBarcode(request);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"出库扫描操作失败: {ex.Message}");
            }
        }
        [HttpPost, Route("QueryPickingTasks"), AllowAnonymous]
        public WebResponseContent QueryPickingTasks(string palletCode, string orderNo)
        {
            return _outboundService.QueryPickingTasks(palletCode, orderNo);
        }
        [HttpPost, Route("QueryPickedList"), AllowAnonymous]
        public WebResponseContent QueryPickedList(string orderNo, string palletCode)
        {
            return _outboundService.QueryPickedList(orderNo, palletCode);
        }
    }
}