8 Komitmen adf8c99d3f ... c1b3445123

Pembuat SHA1 Pesan Tanggal
  luobo c1b3445123 fix:增加warehouseName字段返回 5 hari lalu
  luobo f6b0ae1f0a feat:库存查询接口 5 hari lalu
  luobo 69ebb3e4b4 feat:领料单生成出库单 1 Minggu lalu
  luobo 567501f40b fix:纠正库存number和normalnumber的语义 1 Minggu lalu
  luobo 0f85127783 fix:使用工具类做实体转换,material_requisition表增加sale_order_id字段,用于库存锁传递 1 Minggu lalu
  luobo d3e4b17f6d 删除明细表租户id,增加specification规格型号字段 1 Minggu lalu
  luobo 177540d9e1 feat:领料单新增、修改、删除 1 Minggu lalu
  luobo 9cfe4b7365 fix 1 Minggu lalu
19 mengubah file dengan 1277 tambahan dan 269 penghapusan
  1. 48 0
      easydo-mes/src/main/java/easydo/technology/controller/MaterialRequisitionController.java
  2. 47 0
      easydo-mes/src/main/java/easydo/technology/controller/WarehouseMaterialController.java
  3. 5 0
      easydo-mes/src/main/java/easydo/technology/enums/MESEnum.java
  4. 11 121
      easydo-mes/src/main/java/easydo/technology/model/MaterialRequisition.java
  5. 6 89
      easydo-mes/src/main/java/easydo/technology/model/MaterialRequisitionItem.java
  6. 1 0
      easydo-mes/src/main/java/easydo/technology/model/ProductOrderDispatch.java
  7. 5 2
      easydo-mes/src/main/java/easydo/technology/model/WarehouseMaterial.java
  8. 26 0
      easydo-mes/src/main/java/easydo/technology/model/WarehouseOutbound.java
  9. 25 0
      easydo-mes/src/main/java/easydo/technology/model/WarehouseOutboundItem.java
  10. 23 0
      easydo-mes/src/main/java/easydo/technology/model/vo/MaterialRequisitionOutboundVO.java
  11. 3 0
      easydo-mes/src/main/java/easydo/technology/model/vo/MaterialRequisitionVO.java
  12. 3 0
      easydo-mes/src/main/java/easydo/technology/model/vo/ProductOrderDispatchVO.java
  13. 57 0
      easydo-mes/src/main/java/easydo/technology/service/FlowNoService.java
  14. 27 0
      easydo-mes/src/main/java/easydo/technology/service/MaterialRequisitionService.java
  15. 10 0
      easydo-mes/src/main/java/easydo/technology/service/WarehouseMaterialService.java
  16. 838 1
      easydo-mes/src/main/java/easydo/technology/service/impl/MaterialRequisitionServiceImpl.java
  17. 31 54
      easydo-mes/src/main/java/easydo/technology/service/impl/ProductOrderDispatchServiceImpl.java
  18. 2 2
      easydo-mes/src/main/java/easydo/technology/service/impl/ProductPrePlanServiceImpl.java
  19. 109 0
      easydo-mes/src/main/java/easydo/technology/service/impl/WarehouseMaterialServiceImpl.java

+ 48 - 0
easydo-mes/src/main/java/easydo/technology/controller/MaterialRequisitionController.java

@@ -1,5 +1,7 @@
 package easydo.technology.controller;
 
+import easydo.technology.model.vo.MaterialRequisitionOutboundVO;
+import easydo.technology.model.vo.MaterialRequisitionVO;
 import easydo.technology.service.MaterialRequisitionService;
 import easydo.technology.utils.SecurityUtils;
 import org.springframework.http.HttpStatus;
@@ -31,4 +33,50 @@ public class MaterialRequisitionController {
         Map<String, Object> result = materialRequisitionService.getPage(map);
         return new ResponseEntity<>(result, HttpStatus.OK);
     }
+
+    /**
+     * 新增领料单
+     */
+    @RequestMapping(value = "/save")
+    public Object save(@RequestBody MaterialRequisitionVO model) throws Exception {
+        Long userId = SecurityUtils.getCurrentUserId();
+        model.setCreateId(userId);
+        return new ResponseEntity<>(materialRequisitionService.save(model), HttpStatus.OK);
+    }
+
+    /**
+     * 修改领料单
+     */
+    @RequestMapping(value = "/update")
+    public Object update(@RequestBody MaterialRequisitionVO model) throws Exception {
+        Long userId = SecurityUtils.getCurrentUserId();
+        model.setUpdateId(userId);
+        return new ResponseEntity<>(materialRequisitionService.update(model), HttpStatus.OK);
+    }
+
+    /**
+     * 删除领料单
+     */
+    @RequestMapping(value = "/remove")
+    public Object remove(@RequestBody MaterialRequisitionVO model) throws Exception {
+        Long userId = SecurityUtils.getCurrentUserId();
+        return new ResponseEntity<>(materialRequisitionService.remove(model), HttpStatus.OK);
+    }
+
+    /**
+     * 领料单生成出库单并锁定库存
+     */
+    @RequestMapping(value = "/generateOutbound")
+    public Object generateOutbound(@RequestBody MaterialRequisitionOutboundVO vo) throws Exception {
+        return new ResponseEntity<>(materialRequisitionService.generateOutbound(vo), HttpStatus.OK);
+    }
+
+    /**
+     * 统计领料单物料库存
+     */
+    @RequestMapping(value = "/getMaterialStock")
+    public Object getMaterialStock(@RequestBody Map<String, Object> map) throws Exception {
+        String requisitionId = (String) map.get("requisitionId");
+        return new ResponseEntity<>(materialRequisitionService.getRequisitionMaterialStock(requisitionId), HttpStatus.OK);
+    }
 }

+ 47 - 0
easydo-mes/src/main/java/easydo/technology/controller/WarehouseMaterialController.java

@@ -0,0 +1,47 @@
+package easydo.technology.controller;
+
+import easydo.technology.service.WarehouseMaterialService;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/warehouseMaterial")
+public class WarehouseMaterialController {
+
+    @Resource
+    private WarehouseMaterialService warehouseMaterialService;
+
+    @PostMapping(value = "/getPage")
+    public Object getPage(@RequestBody(required = false) Map<String, Object> map) {
+        try {
+            if (map == null) {
+                map = new HashMap<>();
+            }
+            Map<String, Object> page = warehouseMaterialService.getPage(map);
+            return new ResponseEntity<>(page, HttpStatus.OK);
+        } catch (Exception e) {
+            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    @PostMapping(value = "/getList")
+    public Object getList(@RequestBody(required = false) Map<String, Object> map) {
+        try {
+            if (map == null) {
+                map = new HashMap<>();
+            }
+            Object list = warehouseMaterialService.getList(map);
+            return new ResponseEntity<>(list, HttpStatus.OK);
+        } catch (Exception e) {
+            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+        }
+    }
+}

+ 5 - 0
easydo-mes/src/main/java/easydo/technology/enums/MESEnum.java

@@ -67,6 +67,7 @@ public enum MESEnum {
     FLOW_NO_TYPE_PRODUCT_ORDER("product_order", "工单"),
     FLOW_NO_TYPE_PRODUCT_ORDER_DISPATCH("product_order_dispatch", "派工单"),
     FLOW_NO_TYPE_MATERIAL_REQUISITION("material_requisition", "领料单"),
+    FLOW_NO_TYPE_WAREHOUSE_OUTBOUND("warehouse_outbound", "出库单"),
 
     PRODUCT_PLAN_OF_STATUS_PENDING("pending", "待进行"),
     PRODUCT_PLAN_OF_STATUS_PROCESSING("processing", "进行中"),
@@ -129,6 +130,10 @@ public enum MESEnum {
     WAREHOUSE_RECORD_OF_REF_TYPE_PRODUCT("product", "生产计划"),
     WAREHOUSE_RECORD_OF_REF_TYPE_PURCHASE("purchase", "采购计划"),
     WAREHOUSE_RECORD_OF_REF_TYPE_OUTSOURCING("outsourcing", "委外计划"),
+    WAREHOUSE_RECORD_OF_REF_TYPE_REQUISITION("requisition", "领料单"),
+
+    WAREHOUSE_OUTBOUND_LOCK_SOURCE_SALE("sale_order", "销售订单锁定"),
+    WAREHOUSE_OUTBOUND_LOCK_SOURCE_REQUISITION("requisition", "领料单现场锁定"),
 
     OUTSOURCING_PLAN_OF_TYPE_PROCESS("process", "工序委外"),
     OUTSOURCING_PLAN_OF_TYPE_WHOLE("whole", "整单委外"),

+ 11 - 121
easydo-mes/src/main/java/easydo/technology/model/MaterialRequisition.java

@@ -1,143 +1,33 @@
 package easydo.technology.model;
 
+import easydo.technology.annotation.Minio;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
 /**
  * 领料单主表
  */
-public class MaterialRequisition {
+@Data
+@Minio
+@EqualsAndHashCode(callSuper = true)
+public class MaterialRequisition extends CommonModel{
     
     private String id;
     private String code;
+    private String name;
     private String dispatchId;
     private String orderId;
+    private String saleOrderId;
     private String bomId;
     private String requisitionType;
     private String outboundStatus;
     private String status;
     private String requisitionDate;
+    private Long receiverId;
     private String remark;
     private String tenantId;
     private Long createId;
     private String createTime;
     private Long updateId;
     private String updateTime;
-
-    public String getId() {
-        return id;
-    }
-
-    public void setId(String id) {
-        this.id = id;
-    }
-
-    public String getCode() {
-        return code;
-    }
-
-    public void setCode(String code) {
-        this.code = code;
-    }
-
-    public String getDispatchId() {
-        return dispatchId;
-    }
-
-    public void setDispatchId(String dispatchId) {
-        this.dispatchId = dispatchId;
-    }
-
-    public String getOrderId() {
-        return orderId;
-    }
-
-    public void setOrderId(String orderId) {
-        this.orderId = orderId;
-    }
-
-    public String getBomId() {
-        return bomId;
-    }
-
-    public void setBomId(String bomId) {
-        this.bomId = bomId;
-    }
-
-    public String getRequisitionType() {
-        return requisitionType;
-    }
-
-    public void setRequisitionType(String requisitionType) {
-        this.requisitionType = requisitionType;
-    }
-
-    public String getOutboundStatus() {
-        return outboundStatus;
-    }
-
-    public void setOutboundStatus(String outboundStatus) {
-        this.outboundStatus = outboundStatus;
-    }
-
-    public String getStatus() {
-        return status;
-    }
-
-    public void setStatus(String status) {
-        this.status = status;
-    }
-
-    public String getRequisitionDate() {
-        return requisitionDate;
-    }
-
-    public void setRequisitionDate(String requisitionDate) {
-        this.requisitionDate = requisitionDate;
-    }
-
-    public String getRemark() {
-        return remark;
-    }
-
-    public void setRemark(String remark) {
-        this.remark = remark;
-    }
-
-    public String getTenantId() {
-        return tenantId;
-    }
-
-    public void setTenantId(String tenantId) {
-        this.tenantId = tenantId;
-    }
-
-    public Long getCreateId() {
-        return createId;
-    }
-
-    public void setCreateId(Long createId) {
-        this.createId = createId;
-    }
-
-    public String getCreateTime() {
-        return createTime;
-    }
-
-    public void setCreateTime(String createTime) {
-        this.createTime = createTime;
-    }
-
-    public Long getUpdateId() {
-        return updateId;
-    }
-
-    public void setUpdateId(Long updateId) {
-        this.updateId = updateId;
-    }
-
-    public String getUpdateTime() {
-        return updateTime;
-    }
-
-    public void setUpdateTime(String updateTime) {
-        this.updateTime = updateTime;
-    }
 }

+ 6 - 89
easydo-mes/src/main/java/easydo/technology/model/MaterialRequisitionItem.java

@@ -1,109 +1,26 @@
 package easydo.technology.model;
 
+import lombok.Data;
+
 import java.math.BigDecimal;
 
 /**
  * 领料单明细表
  */
+@Data
 public class MaterialRequisitionItem {
     
     private String id;
     private String requisitionId;
     private String materialCode;
     private String materialName;
+    private String specification;
     private BigDecimal requiredQuantity;
     private BigDecimal actualQuantity;
+    private BigDecimal returnQuantity;
+    private BigDecimal wasteQuantity;
     private String unit;
     private String warehouseId;
     private String remark;
-    private String tenantId;
     private String createTime;
-
-    public String getId() {
-        return id;
-    }
-
-    public void setId(String id) {
-        this.id = id;
-    }
-
-    public String getRequisitionId() {
-        return requisitionId;
-    }
-
-    public void setRequisitionId(String requisitionId) {
-        this.requisitionId = requisitionId;
-    }
-
-    public String getMaterialCode() {
-        return materialCode;
-    }
-
-    public void setMaterialCode(String materialCode) {
-        this.materialCode = materialCode;
-    }
-
-    public String getMaterialName() {
-        return materialName;
-    }
-
-    public void setMaterialName(String materialName) {
-        this.materialName = materialName;
-    }
-
-    public BigDecimal getRequiredQuantity() {
-        return requiredQuantity;
-    }
-
-    public void setRequiredQuantity(BigDecimal requiredQuantity) {
-        this.requiredQuantity = requiredQuantity;
-    }
-
-    public BigDecimal getActualQuantity() {
-        return actualQuantity;
-    }
-
-    public void setActualQuantity(BigDecimal actualQuantity) {
-        this.actualQuantity = actualQuantity;
-    }
-
-    public String getUnit() {
-        return unit;
-    }
-
-    public void setUnit(String unit) {
-        this.unit = unit;
-    }
-
-    public String getWarehouseId() {
-        return warehouseId;
-    }
-
-    public void setWarehouseId(String warehouseId) {
-        this.warehouseId = warehouseId;
-    }
-
-    public String getRemark() {
-        return remark;
-    }
-
-    public void setRemark(String remark) {
-        this.remark = remark;
-    }
-
-    public String getTenantId() {
-        return tenantId;
-    }
-
-    public void setTenantId(String tenantId) {
-        this.tenantId = tenantId;
-    }
-
-    public String getCreateTime() {
-        return createTime;
-    }
-
-    public void setCreateTime(String createTime) {
-        this.createTime = createTime;
-    }
 }

+ 1 - 0
easydo-mes/src/main/java/easydo/technology/model/ProductOrderDispatch.java

@@ -18,6 +18,7 @@ public class ProductOrderDispatch extends CommonModel {
     private String code;
     private String name;
     private String orderId;
+    private String saleOrderId;
     private String beginDate;
     private String endDate;
     private String status;

+ 5 - 2
easydo-mes/src/main/java/easydo/technology/model/WarehouseMaterial.java

@@ -13,8 +13,11 @@ public class WarehouseMaterial extends CommonModel {
     private String warehouseId;
     private String materialCode;
 
+    @NotTableField
+    private String warehouseName;
+
     /**
-     * 可用库存数量(对应数据库 decimal)
+     * 库存数量(对应数据库 decimal)
      */
     private BigDecimal number;
 
@@ -29,7 +32,7 @@ public class WarehouseMaterial extends CommonModel {
     private BigDecimal frozenNumber;
 
     /**
-     * 正常数量
+     * 可用数量
      */
     private BigDecimal normalNumber;
 

+ 26 - 0
easydo-mes/src/main/java/easydo/technology/model/WarehouseOutbound.java

@@ -0,0 +1,26 @@
+package easydo.technology.model;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 出库单主表
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class WarehouseOutbound extends CommonModel {
+
+    private String id;
+    private String code;
+    private String requisitionId;
+    private String orderId;
+    private String dispatchId;
+    private String bomId;
+    private String saleOrderId;
+    private String status;
+    private String tenantId;
+    private Long createId;
+    private String createTime;
+    private Long updateId;
+    private String updateTime;
+}

+ 25 - 0
easydo-mes/src/main/java/easydo/technology/model/WarehouseOutboundItem.java

@@ -0,0 +1,25 @@
+package easydo.technology.model;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 出库单明细表
+ */
+@Data
+public class WarehouseOutboundItem {
+
+    private String id;
+    private String outboundId;
+    private String materialCode;
+    private BigDecimal requiredQuantity;
+    private BigDecimal actualQuantity;
+    private String lockSource;
+    private String warehouseId;
+    private String tenantId;
+    private Long createId;
+    private String createTime;
+    private Long updateId;
+    private String updateTime;
+}

+ 23 - 0
easydo-mes/src/main/java/easydo/technology/model/vo/MaterialRequisitionOutboundVO.java

@@ -0,0 +1,23 @@
+package easydo.technology.model.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 领料单生成出库单请求
+ */
+@Data
+public class MaterialRequisitionOutboundVO {
+
+    private String requisitionId;
+    private List<OutboundItem> items;
+
+    @Data
+    public static class OutboundItem {
+        private String materialCode;
+        private BigDecimal quantity;
+        private String warehouseId;
+    }
+}

+ 3 - 0
easydo-mes/src/main/java/easydo/technology/model/vo/MaterialRequisitionVO.java

@@ -13,13 +13,16 @@ public class MaterialRequisitionVO {
     
     private String id;
     private String code;
+    private String name;
     private String dispatchId;
     private String orderId;
+    private String saleOrderId;
     private String bomId;
     private String requisitionType;
     private String outboundStatus;
     private String status;
     private String requisitionDate;
+    private Long receiverId;
     private String remark;
     private String tenantId;
     private Long createId;

+ 3 - 0
easydo-mes/src/main/java/easydo/technology/model/vo/ProductOrderDispatchVO.java

@@ -70,13 +70,16 @@ public class ProductOrderDispatchVO {
     public static class SimpleMaterialRequisition {
         private String id;
         private String code;
+        private String name;
         private String dispatchId;
         private String orderId;
+        private String saleOrderId;
         private String bomId;
         private String requisitionType;  // auto-领料, manual-补料
         private String outboundStatus;   // applied-申请完毕, pending-未出库, partial-部分出库, completed-出库完毕
         private String status;           // pending-未领料, complete-已领料
         private String requisitionDate;
+        private Long receiverId;
         private String remark;
         private String tenantId;
         private Long createId;

+ 57 - 0
easydo-mes/src/main/java/easydo/technology/service/FlowNoService.java

@@ -41,6 +41,7 @@ public class FlowNoService {
     private final Object productOrderLock = new Object();
     private final Object productOrderDispatchLock = new Object();
     private final Object materialRequisitionLock = new Object();
+    private final Object warehouseOutboundLock = new Object();
 
 
     /**
@@ -875,6 +876,62 @@ public class FlowNoService {
         }
     }
 
+    /**
+     * 生成出库单编码 (WarehouseOutbound) - 独立解耦实现
+     */
+    public String generateWarehouseOutboundCode(WarehouseOutbound model, Connection connection) throws Exception {
+        synchronized (warehouseOutboundLock) {
+            String manualCode = model.getCode();
+            String tenantId = model.getTenantId();
+            if (StringUtil.isNotEmpty(manualCode)) {
+                Map<String, Object> checkMap = new HashMap<>();
+                checkMap.put("code", manualCode);
+                checkMap.put("tenantId", tenantId);
+                int count = (int) jdbcClient.getJdbcCountByMap(checkMap, WarehouseOutbound.class, connection);
+                if (count > 0) {
+                    throw new BizException("出库单编号已存在: " + manualCode);
+                }
+                return manualCode;
+            }
+
+            while (true) {
+                FlowNo flowNo = new FlowNo();
+                flowNo.setType(MESEnum.FLOW_NO_TYPE_WAREHOUSE_OUTBOUND.getValue());
+                flowNo.setTenantId(tenantId);
+                flowNo = jdbcClient.getJdbcModel(flowNo, connection);
+                if (flowNo == null) throw new BizException("未配置出库单流水号规则");
+
+                String currDate = DFY_MD.format(LocalDateTime.now());
+                String currDate2 = DFY_MD_2.format(LocalDateTime.now());
+                if (StringUtil.isEmpty(flowNo.getCurrDate()) || !flowNo.getCurrDate().equals(currDate)) {
+                    flowNo.setCurrDate(currDate);
+                    flowNo.setCurrSeq(1);
+                } else {
+                    flowNo.setCurrSeq(flowNo.getCurrSeq() + 1);
+                }
+
+                String no;
+                if (StringUtil.isEmpty(flowNo.getCurrDate())) {
+                    no = (flowNo.getPrefix() != null ? flowNo.getPrefix() : "") + String.format("%06d", flowNo.getCurrSeq());
+                } else {
+                    no = (flowNo.getPrefix() != null ? flowNo.getPrefix() : "") + currDate2 + String.format("%05d", flowNo.getCurrSeq());
+                }
+                flowNo.setCurrNo(no);
+
+                Map<String, Object> updateMap = new HashMap<>();
+                updateMap.put("type", MESEnum.FLOW_NO_TYPE_WAREHOUSE_OUTBOUND.getValue());
+                updateMap.put("tenantId", tenantId);
+                jdbcClient.jdbcUpdate(flowNo, updateMap, connection);
+
+                Map<String, Object> checkMap = new HashMap<>();
+                checkMap.put("code", no);
+                checkMap.put("tenantId", tenantId);
+                int count = (int) jdbcClient.getJdbcCountByMap(checkMap, WarehouseOutbound.class, connection);
+                if (count == 0) return no;
+            }
+        }
+    }
+
     /**
      * 原始流水号生成方法
      */

+ 27 - 0
easydo-mes/src/main/java/easydo/technology/service/MaterialRequisitionService.java

@@ -1,6 +1,8 @@
 package easydo.technology.service;
 
 import easydo.technology.model.ProductOrderDispatch;
+import easydo.technology.model.vo.MaterialRequisitionOutboundVO;
+import easydo.technology.model.vo.MaterialRequisitionVO;
 
 import java.sql.Connection;
 import java.util.Map;
@@ -16,6 +18,21 @@ public interface MaterialRequisitionService {
      * @return 分页结果
      */
     Map<String, Object> getPage(Map<String, Object> map) throws Exception;
+
+    /**
+     * 新增领料单
+     */
+    MaterialRequisitionVO save(MaterialRequisitionVO model) throws Exception;
+
+    /**
+     * 修改领料单
+     */
+    MaterialRequisitionVO update(MaterialRequisitionVO model) throws Exception;
+
+    /**
+     * 删除领料单
+     */
+    MaterialRequisitionVO remove(MaterialRequisitionVO model) throws Exception;
     
     /**
      * 生成领料单(在派工时调用)
@@ -31,4 +48,14 @@ public interface MaterialRequisitionService {
      * @param connection 外部事务的 connection
      */
     void updateStatus(String requisitionId, String status, Connection connection) throws Exception;
+
+    /**
+     * 领料单生成出库单并锁定库存
+     */
+    String generateOutbound(MaterialRequisitionOutboundVO vo) throws Exception;
+
+    /**
+     * 统计领料单物料库存数量
+     */
+    Map<String, Object> getRequisitionMaterialStock(String requisitionId) throws Exception;
 }

+ 10 - 0
easydo-mes/src/main/java/easydo/technology/service/WarehouseMaterialService.java

@@ -0,0 +1,10 @@
+package easydo.technology.service;
+
+import java.util.Map;
+
+public interface WarehouseMaterialService {
+
+    Map<String, Object> getPage(Map<String, Object> map) throws Exception;
+
+    Object getList(Map<String, Object> map) throws Exception;
+}

+ 838 - 1
easydo-mes/src/main/java/easydo/technology/service/impl/MaterialRequisitionServiceImpl.java

@@ -4,9 +4,11 @@ import easydo.technology.components.JdbcClient;
 import easydo.technology.enums.MESEnum;
 import easydo.technology.exception.BizException;
 import easydo.technology.model.*;
+import easydo.technology.model.vo.MaterialRequisitionOutboundVO;
 import easydo.technology.model.vo.MaterialRequisitionVO;
 import easydo.technology.service.FlowNoService;
 import easydo.technology.service.MaterialRequisitionService;
+import easydo.technology.system.model.SysUser;
 import easydo.technology.utils.SecurityUtils;
 import org.springframework.stereotype.Service;
 
@@ -32,6 +34,7 @@ public class MaterialRequisitionServiceImpl implements MaterialRequisitionServic
 
     private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
     private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+    private static final BigDecimal ZERO = BigDecimal.ZERO;
 
     @Override
     @SuppressWarnings("unchecked")
@@ -54,13 +57,16 @@ public class MaterialRequisitionServiceImpl implements MaterialRequisitionServic
                 // 复制主要字段
                 vo.setId(model.getId());
                 vo.setCode(model.getCode());
+                vo.setName(model.getName());
                 vo.setDispatchId(model.getDispatchId());
                 vo.setOrderId(model.getOrderId());
+                vo.setSaleOrderId(model.getSaleOrderId());
                 vo.setBomId(model.getBomId());
                 vo.setRequisitionType(model.getRequisitionType());
                 vo.setOutboundStatus(model.getOutboundStatus());
                 vo.setStatus(model.getStatus());
                 vo.setRequisitionDate(model.getRequisitionDate());
+                vo.setReceiverId(model.getReceiverId());
                 vo.setRemark(model.getRemark());
                 vo.setTenantId(model.getTenantId());
                 vo.setCreateId(model.getCreateId());
@@ -87,6 +93,165 @@ public class MaterialRequisitionServiceImpl implements MaterialRequisitionServic
         }
     }
 
+    @Override
+    public MaterialRequisitionVO save(MaterialRequisitionVO model) throws Exception {
+        Connection connection = dataSource.getConnection();
+        connection.setAutoCommit(false);
+        try {
+            String tenantId = resolveTenantId(model, connection);
+
+            MaterialRequisition requisition = new MaterialRequisition();
+            requisition.setCode(model.getCode());
+            requisition.setName(model.getName());
+            requisition.setDispatchId(model.getDispatchId());
+            requisition.setOrderId(model.getOrderId());
+            requisition.setSaleOrderId(model.getSaleOrderId());
+            requisition.setBomId(model.getBomId());
+            requisition.setRequisitionType(model.getRequisitionType() != null && !model.getRequisitionType().trim().isEmpty()
+                ? model.getRequisitionType()
+                : MESEnum.MATERIAL_REQUISITION_TYPE_MANUAL.getValue());
+            requisition.setOutboundStatus(model.getOutboundStatus() != null && !model.getOutboundStatus().trim().isEmpty()
+                ? model.getOutboundStatus()
+                : MESEnum.MATERIAL_REQUISITION_OUTBOUND_STATUS_APPLIED.getValue());
+            requisition.setStatus(model.getStatus() != null && !model.getStatus().trim().isEmpty()
+                ? model.getStatus()
+                : MESEnum.MATERIAL_REQUISITION_OF_STATUS_PENDING.getValue());
+            requisition.setRequisitionDate(model.getRequisitionDate());
+            requisition.setReceiverId(model.getReceiverId());
+            requisition.setRemark(model.getRemark());
+            requisition.setTenantId(tenantId);
+            requisition.setCreateId(model.getCreateId());
+            requisition.setCreateTime(FORMATTER.format(LocalDateTime.now()));
+
+            String requisitionCode = flowNoService.generateMaterialRequisitionCode(requisition, connection);
+            requisition.setCode(requisitionCode);
+            jdbcClient.jdbcInsert(requisition, connection);
+
+            insertItems(requisition.getId(), tenantId, model.getItems(), connection);
+
+            connection.commit();
+            model.setId(requisition.getId());
+            model.setCode(requisition.getCode());
+            model.setRequisitionType(requisition.getRequisitionType());
+            model.setOutboundStatus(requisition.getOutboundStatus());
+            model.setStatus(requisition.getStatus());
+            model.setTenantId(requisition.getTenantId());
+            model.setCreateTime(requisition.getCreateTime());
+            return model;
+        } catch (Exception e) {
+            connection.rollback();
+            throw new BizException(e.getMessage());
+        } finally {
+            connection.close();
+        }
+    }
+
+    @Override
+    public MaterialRequisitionVO update(MaterialRequisitionVO model) throws Exception {
+        Connection connection = dataSource.getConnection();
+        connection.setAutoCommit(false);
+        try {
+            if (model.getId() == null || model.getId().trim().isEmpty()) {
+                throw new BizException("领料单ID不能为空");
+            }
+
+            MaterialRequisition query = new MaterialRequisition();
+            query.setId(model.getId());
+            MaterialRequisition requisition = jdbcClient.getJdbcModelById(query, connection);
+            if (requisition == null) {
+                throw new BizException("领料单不存在");
+            }
+
+            if (model.getDispatchId() == null || model.getDispatchId().trim().isEmpty()) {
+                model.setDispatchId(requisition.getDispatchId());
+            }
+            if (model.getOrderId() == null || model.getOrderId().trim().isEmpty()) {
+                model.setOrderId(requisition.getOrderId());
+            }
+            if (model.getSaleOrderId() == null || model.getSaleOrderId().trim().isEmpty()) {
+                model.setSaleOrderId(requisition.getSaleOrderId());
+            }
+            if (model.getBomId() == null || model.getBomId().trim().isEmpty()) {
+                model.setBomId(requisition.getBomId());
+            }
+            if (model.getTenantId() == null || model.getTenantId().trim().isEmpty()) {
+                model.setTenantId(requisition.getTenantId());
+            }
+
+            String tenantId = resolveTenantId(model, connection);
+
+            requisition.setCode(model.getCode());
+            requisition.setName(model.getName());
+            requisition.setDispatchId(model.getDispatchId());
+            requisition.setOrderId(model.getOrderId());
+            requisition.setSaleOrderId(model.getSaleOrderId());
+            requisition.setBomId(model.getBomId());
+            requisition.setRequisitionType(model.getRequisitionType() != null && !model.getRequisitionType().trim().isEmpty()
+                ? model.getRequisitionType()
+                : requisition.getRequisitionType());
+            requisition.setOutboundStatus(model.getOutboundStatus() != null && !model.getOutboundStatus().trim().isEmpty()
+                ? model.getOutboundStatus()
+                : requisition.getOutboundStatus());
+            requisition.setStatus(model.getStatus() != null && !model.getStatus().trim().isEmpty()
+                ? model.getStatus()
+                : requisition.getStatus());
+            requisition.setRequisitionDate(model.getRequisitionDate());
+            requisition.setReceiverId(model.getReceiverId());
+            requisition.setRemark(model.getRemark());
+            requisition.setTenantId(tenantId);
+            requisition.setUpdateId(model.getUpdateId());
+            requisition.setUpdateTime(FORMATTER.format(LocalDateTime.now()));
+            jdbcClient.jdbcUpdateById(requisition, connection);
+
+            MaterialRequisitionItem removeItemParam = new MaterialRequisitionItem();
+            removeItemParam.setRequisitionId(requisition.getId());
+            jdbcClient.jdbcRemove(removeItemParam, connection);
+            insertItems(requisition.getId(), tenantId, model.getItems(), connection);
+
+            connection.commit();
+            model.setCode(requisition.getCode());
+            model.setTenantId(requisition.getTenantId());
+            model.setUpdateTime(requisition.getUpdateTime());
+            return model;
+        } catch (Exception e) {
+            connection.rollback();
+            throw new BizException(e.getMessage());
+        } finally {
+            connection.close();
+        }
+    }
+
+    @Override
+    public MaterialRequisitionVO remove(MaterialRequisitionVO model) throws Exception {
+        Connection connection = dataSource.getConnection();
+        connection.setAutoCommit(false);
+        try {
+            if (model.getId() == null || model.getId().trim().isEmpty()) {
+                throw new BizException("领料单ID不能为空");
+            }
+
+            MaterialRequisition requisition = new MaterialRequisition();
+            requisition.setId(model.getId());
+            MaterialRequisition exist = jdbcClient.getJdbcModelById(requisition, connection);
+            if (exist == null) {
+                throw new BizException("领料单不存在");
+            }
+
+            MaterialRequisitionItem removeItemParam = new MaterialRequisitionItem();
+            removeItemParam.setRequisitionId(model.getId());
+            jdbcClient.jdbcRemove(removeItemParam, connection);
+            jdbcClient.jdbcRemoveById(requisition, connection);
+
+            connection.commit();
+            return model;
+        } catch (Exception e) {
+            connection.rollback();
+            throw new BizException(e.getMessage());
+        } finally {
+            connection.close();
+        }
+    }
+
     @Override
     public void generateRequisition(ProductOrderDispatch dispatch, Connection connection) throws Exception {
         try {
@@ -145,6 +310,7 @@ public class MaterialRequisitionServiceImpl implements MaterialRequisitionServic
             MaterialRequisition requisition = new MaterialRequisition();
             requisition.setDispatchId(dispatch.getId());
             requisition.setOrderId(dispatch.getOrderId());
+            requisition.setSaleOrderId(dispatch.getSaleOrderId());
             requisition.setBomId(dispatch.getBomId());
             requisition.setRequisitionType(MESEnum.MATERIAL_REQUISITION_TYPE_AUTO.getValue());  // 自动生成
             requisition.setOutboundStatus(MESEnum.MATERIAL_REQUISITION_OUTBOUND_STATUS_APPLIED.getValue());  // 申请完毕
@@ -157,6 +323,7 @@ public class MaterialRequisitionServiceImpl implements MaterialRequisitionServic
             // 生成领料单编号
             String requisitionCode = flowNoService.generateMaterialRequisitionCode(requisition, connection);
             requisition.setCode(requisitionCode);
+            requisition.setName(requisitionCode);
             
             jdbcClient.jdbcInsert(requisition, connection);
             
@@ -176,6 +343,7 @@ public class MaterialRequisitionServiceImpl implements MaterialRequisitionServic
                 
                 // 查询物料信息获取单位
                 String unit = null;
+                String specification = null;
                 if (subBom.getMaterialCode() != null) {
                     ProductMaterial materialQuery = new ProductMaterial();
                     materialQuery.setCode(subBom.getMaterialCode());
@@ -183,11 +351,12 @@ public class MaterialRequisitionServiceImpl implements MaterialRequisitionServic
                     List<ProductMaterial> materials = jdbcClient.getJdbcList(materialQuery, connection);
                     if (materials != null && !materials.isEmpty()) {
                         unit = materials.get(0).getUnit();
+                        specification = materials.get(0).getSpecification();
                     }
                 }
                 item.setUnit(unit);
+                item.setSpecification(specification);
                 
-                item.setTenantId(dispatch.getTenantId());
                 item.setCreateTime(FORMATTER.format(LocalDateTime.now()));
                 
                 jdbcClient.jdbcInsert(item, connection);
@@ -220,4 +389,672 @@ public class MaterialRequisitionServiceImpl implements MaterialRequisitionServic
             throw new BizException("更新领料单状态失败: " + e.getMessage());
         }
     }
+
+    private String resolveTenantId(MaterialRequisitionVO model, Connection connection) throws Exception {
+        if (model.getDispatchId() != null && !model.getDispatchId().trim().isEmpty()) {
+            ProductOrderDispatch dispatchQuery = new ProductOrderDispatch();
+            dispatchQuery.setId(model.getDispatchId());
+            ProductOrderDispatch dispatch = jdbcClient.getJdbcModelById(dispatchQuery, connection);
+            if (dispatch == null) {
+                throw new BizException("关联派工单不存在");
+            }
+
+            if (model.getOrderId() == null || model.getOrderId().trim().isEmpty()) {
+                model.setOrderId(dispatch.getOrderId());
+            } else if (!model.getOrderId().equals(dispatch.getOrderId())) {
+                throw new BizException("领料单工单与派工单不匹配");
+            }
+
+            if (model.getBomId() == null || model.getBomId().trim().isEmpty()) {
+                model.setBomId(dispatch.getBomId());
+            } else if (!model.getBomId().equals(dispatch.getBomId())) {
+                throw new BizException("领料单BOM与派工单不匹配");
+            }
+
+            if (dispatch.getTenantId() != null && !dispatch.getTenantId().trim().isEmpty()) {
+                model.setTenantId(dispatch.getTenantId());
+                return dispatch.getTenantId();
+            }
+        }
+
+        boolean hasOrderId = model.getOrderId() != null && !model.getOrderId().trim().isEmpty();
+        boolean hasBomId = model.getBomId() != null && !model.getBomId().trim().isEmpty();
+        if (hasOrderId || hasBomId) {
+            if (!hasOrderId || !hasBomId) {
+                throw new BizException("工单ID和BOM ID需要同时填写");
+            }
+
+            ProductOrder orderQuery = new ProductOrder();
+            orderQuery.setId(model.getOrderId());
+            ProductOrder order = jdbcClient.getJdbcModelById(orderQuery, connection);
+            if (order == null) {
+                throw new BizException("工单不存在");
+            }
+
+            ProductOrderBom bomQuery = new ProductOrderBom();
+            bomQuery.setOrderId(model.getOrderId());
+            bomQuery.setBomId(model.getBomId());
+            List<ProductOrderBom> orderBomList = jdbcClient.getJdbcList(bomQuery, connection);
+            if (orderBomList == null || orderBomList.isEmpty()) {
+                throw new BizException("工单中不存在该BOM");
+            }
+
+            model.setTenantId(order.getTenantId());
+            return order.getTenantId();
+        }
+
+        if (model.getTenantId() != null && !model.getTenantId().trim().isEmpty()) {
+            return model.getTenantId();
+        }
+
+        Long currentUserId = SecurityUtils.getCurrentUserId();
+        SysUser userQuery = new SysUser();
+        userQuery.setId(currentUserId);
+        SysUser user = jdbcClient.getJdbcModelById(userQuery, connection);
+        if (user == null || user.getTenantId() == null || user.getTenantId().trim().isEmpty()) {
+            throw new BizException("无法获取当前用户租户信息");
+        }
+        model.setTenantId(user.getTenantId());
+        return user.getTenantId();
+    }
+
+    private void insertItems(String requisitionId, String tenantId, List<MaterialRequisitionItem> items, Connection connection) throws Exception {
+        if (items == null || items.isEmpty()) {
+            return;
+        }
+
+        for (MaterialRequisitionItem item : items) {
+            item.setId(null);
+            item.setRequisitionId(requisitionId);
+            if (item.getActualQuantity() == null) {
+                item.setActualQuantity(BigDecimal.ZERO);
+            }
+            item.setCreateTime(FORMATTER.format(LocalDateTime.now()));
+            jdbcClient.jdbcInsert(item, connection);
+        }
+    }
+
+    /**
+     * 领料单生成出库单并锁定库存
+     *
+     * 业务逻辑:
+     * 1. 支持灵活出库:可以指定部分物料、部分数量、指定仓库
+     * 2. 验证出库明细的合法性(物料是否在领料单中、数量是否超标)
+     * 3. 创建出库单主表,关联领料单、工单、派工单、BOM、销售订单
+     * 4. 为每个出库明细锁定库存(优先使用销售订单已锁定库存,不足则现场锁定)
+     *
+     * @param vo 出库请求,包含领料单ID和出库明细列表(物料编码、数量、仓库ID)
+     * @return 出库单ID
+     */
+    @Override
+    public String generateOutbound(MaterialRequisitionOutboundVO vo) throws Exception {
+        // 1. 获取当前用户
+        Long userId = SecurityUtils.getCurrentUserId();
+        if (userId == null) {
+            throw new BizException("用户未登录");
+        }
+
+        // 2. 验证入参
+        if (vo.getRequisitionId() == null || vo.getRequisitionId().trim().isEmpty()) {
+            throw new BizException("领料单ID不能为空");
+        }
+        if (vo.getItems() == null || vo.getItems().isEmpty()) {
+            throw new BizException("出库明细不能为空");
+        }
+
+        Connection connection = dataSource.getConnection();
+        connection.setAutoCommit(false);
+        try {
+            // 3. 查询领料单信息
+            MaterialRequisition requisition = getRequisitionForOutbound(vo.getRequisitionId(), connection);
+
+            // 4. 查询领料单明细,用于验证出库明细的合法性
+            MaterialRequisitionItem itemQuery = new MaterialRequisitionItem();
+            itemQuery.setRequisitionId(vo.getRequisitionId());
+            List<MaterialRequisitionItem> requisitionItems = jdbcClient.getJdbcList(itemQuery, connection);
+            if (requisitionItems == null || requisitionItems.isEmpty()) {
+                throw new BizException("领料单明细为空");
+            }
+
+            // 5. 查询该领料单已生成的出库单数量(基于warehouse_record表统计)
+            // 统计领料单现场锁定的数量(ref_type=REQUISITION)
+            Map<String, BigDecimal> existingMaterialMap = new java.util.HashMap<>();
+            for (MaterialRequisitionItem reqItem : requisitionItems) {
+                WarehouseRecord recordQuery = new WarehouseRecord();
+                recordQuery.setRefType(MESEnum.WAREHOUSE_RECORD_OF_REF_TYPE_REQUISITION.getValue());
+                recordQuery.setRefId(vo.getRequisitionId());
+                recordQuery.setMaterialCode(reqItem.getMaterialCode());
+                recordQuery.setType(MESEnum.WAREHOUSE_RECORD_OF_TYPE_LOCK.getValue());
+                List<WarehouseRecord> records = jdbcClient.getJdbcList(recordQuery, connection);
+
+                BigDecimal quantity = ZERO;
+                if (records != null) {
+                    for (WarehouseRecord record : records) {
+                        if (record.getNumber() != null) {
+                            quantity = quantity.add(record.getNumber());
+                        }
+                    }
+                }
+                if (quantity.compareTo(ZERO) > 0) {
+                    existingMaterialMap.put(reqItem.getMaterialCode(), quantity);
+                }
+            }
+
+            // 6. 验证每个出库明细
+            Map<String, BigDecimal> materialTotalMap = new java.util.HashMap<>();
+            for (MaterialRequisitionOutboundVO.OutboundItem outboundItem : vo.getItems()) {
+                // 6.1 验证物料编码
+                if (outboundItem.getMaterialCode() == null || outboundItem.getMaterialCode().trim().isEmpty()) {
+                    throw new BizException("物料编码不能为空");
+                }
+                // 6.2 验证出库数量
+                if (outboundItem.getQuantity() == null || outboundItem.getQuantity().compareTo(ZERO) <= 0) {
+                    throw new BizException("出库数量必须大于0");
+                }
+                // 6.3 验证仓库ID
+                if (outboundItem.getWarehouseId() == null || outboundItem.getWarehouseId().trim().isEmpty()) {
+                    throw new BizException("仓库ID不能为空");
+                }
+
+                // 6.4 验证物料是否在领料单中
+                MaterialRequisitionItem requisitionItem = requisitionItems.stream()
+                    .filter(item -> outboundItem.getMaterialCode().equals(item.getMaterialCode()))
+                    .findFirst()
+                    .orElseThrow(() -> new BizException("物料 " + outboundItem.getMaterialCode() + " 不在领料单中"));
+
+                // 6.5 累计同一物料的出库数量
+                String materialCode = outboundItem.getMaterialCode();
+                BigDecimal currentTotal = materialTotalMap.getOrDefault(materialCode, ZERO);
+                materialTotalMap.put(materialCode, currentTotal.add(outboundItem.getQuantity()));
+            }
+
+            // 6.6 验证每种物料的累计出库数量(已有+本次)不超过领料单需求数量
+            for (Map.Entry<String, BigDecimal> entry : materialTotalMap.entrySet()) {
+                String materialCode = entry.getKey();
+                BigDecimal thisTimeQuantity = entry.getValue();
+                BigDecimal existingQuantity = existingMaterialMap.getOrDefault(materialCode, ZERO);
+                BigDecimal totalQuantity = existingQuantity.add(thisTimeQuantity);
+
+                MaterialRequisitionItem requisitionItem = requisitionItems.stream()
+                    .filter(item -> materialCode.equals(item.getMaterialCode()))
+                    .findFirst()
+                    .orElseThrow(() -> new BizException("物料 " + materialCode + " 不在领料单中"));
+
+                BigDecimal requiredQuantity = requisitionItem.getRequiredQuantity() != null ? requisitionItem.getRequiredQuantity() : ZERO;
+                if (totalQuantity.compareTo(requiredQuantity) > 0) {
+                    throw new BizException("物料 " + materialCode + " 累计出库数量(" + totalQuantity + ")不能超过需求数量(" + requiredQuantity + "),已有出库单数量(" + existingQuantity + ")");
+                }
+            }
+
+            // 6. 创建出库单主表
+            WarehouseOutbound outbound = new WarehouseOutbound();
+            outbound.setRequisitionId(requisition.getId());
+            outbound.setOrderId(requisition.getOrderId());
+            outbound.setDispatchId(requisition.getDispatchId());
+            outbound.setBomId(requisition.getBomId());
+            outbound.setSaleOrderId(requisition.getSaleOrderId());
+            outbound.setStatus(MESEnum.MATERIAL_REQUISITION_OUTBOUND_STATUS_PENDING.getValue());
+            outbound.setTenantId(requisition.getTenantId());
+            outbound.setCreateId(userId);
+            outbound.setCreateTime(FORMATTER.format(LocalDateTime.now()));
+            // 6.1 生成出库单编号
+            String outboundCode = flowNoService.generateWarehouseOutboundCode(outbound, connection);
+            outbound.setCode(outboundCode);
+            jdbcClient.jdbcInsert(outbound, connection);
+
+            // 7. 为每个出库明细创建出库单明细并锁定库存
+            for (MaterialRequisitionOutboundVO.OutboundItem outboundItem : vo.getItems()) {
+                createOutboundItemAndLock(outbound, requisition, outboundItem, userId, connection);
+            }
+
+            connection.commit();
+            return outbound.getId();
+        } catch (Exception e) {
+            connection.rollback();
+            throw new BizException(e.getMessage());
+        } finally {
+            connection.close();
+        }
+    }
+
+    /**
+     * 统计领料单物料库存
+     *
+     * 业务逻辑:
+     * 1. 查询领料单的所有物料明细
+     * 2. 对每个物料,查询其在所有仓库的库存情况
+     * 3. 查询该物料已生成的出库单数量(支持分多次出库)
+     * 4. 按仓库维度组织数据,返回每个物料在每个仓库的库存明细
+     * 5. 包含总库存、可用库存、锁定库存、销售订单可用锁定量、已出库数量、剩余可出数量
+     *
+     * 返回结构:
+     * {
+     *   "materialCode1": {
+     *     "materialCode": "M001",
+     *     "materialName": "物料名称",
+     *     "requiredQuantity": 100,
+     *     "outboundedQuantity": 60,
+     *     "remainingQuantity": 40,
+     *     "unit": "个",
+     *     "warehouses": [
+     *       {
+     *         "warehouseId": "W1",
+     *         "totalQuantity": 200,
+     *         "normalQuantity": 150,
+     *         "lockedQuantity": 50,
+     *         "saleOrderAvailableLocked": 30
+     *       }
+     *     ]
+     *   }
+     * }
+     *
+     * @param requisitionId 领料单ID
+     * @return 物料库存统计,按物料和仓库维度组织
+     */
+    @Override
+    public Map<String, Object> getRequisitionMaterialStock(String requisitionId) throws Exception {
+        Connection connection = dataSource.getConnection();
+        try {
+            // 1. 查询领料单信息
+            MaterialRequisition requisition = getRequisitionForOutbound(requisitionId, connection);
+
+            // 2. 查询领料单明细
+            MaterialRequisitionItem itemQuery = new MaterialRequisitionItem();
+            itemQuery.setRequisitionId(requisitionId);
+            List<MaterialRequisitionItem> items = jdbcClient.getJdbcList(itemQuery, connection);
+            if (items == null || items.isEmpty()) {
+                throw new BizException("领料单明细为空");
+            }
+
+            // 2.1 查询该领料单已生成的出库单数量(基于warehouse_record表统计)
+            // 统计领料单现场锁定的数量(ref_type=REQUISITION)
+            Map<String, BigDecimal> outboundedQuantityMap = new java.util.HashMap<>();
+            for (MaterialRequisitionItem item : items) {
+                WarehouseRecord recordQuery = new WarehouseRecord();
+                recordQuery.setRefType(MESEnum.WAREHOUSE_RECORD_OF_REF_TYPE_REQUISITION.getValue());
+                recordQuery.setRefId(requisitionId);
+                recordQuery.setMaterialCode(item.getMaterialCode());
+                recordQuery.setType(MESEnum.WAREHOUSE_RECORD_OF_TYPE_LOCK.getValue());
+                List<WarehouseRecord> records = jdbcClient.getJdbcList(recordQuery, connection);
+
+                BigDecimal quantity = ZERO;
+                if (records != null) {
+                    for (WarehouseRecord record : records) {
+                        if (record.getNumber() != null) {
+                            quantity = quantity.add(record.getNumber());
+                        }
+                    }
+                }
+                outboundedQuantityMap.put(item.getMaterialCode(), quantity);
+            }
+
+            Map<String, Object> result = new java.util.HashMap<>();
+
+            // 3. 遍历每个物料,统计其在各个仓库的库存
+            for (MaterialRequisitionItem item : items) {
+                String materialCode = item.getMaterialCode();
+
+                // 3.1 获取该物料已生成出库单数量(从前面统计的结果中获取)
+                BigDecimal outboundedQuantity = outboundedQuantityMap.getOrDefault(materialCode, ZERO);
+
+                // 3.2 计算剩余可出数量
+                BigDecimal requiredQuantity = item.getRequiredQuantity() != null ? item.getRequiredQuantity() : ZERO;
+                BigDecimal remainingQuantity = requiredQuantity.subtract(outboundedQuantity);
+                remainingQuantity = remainingQuantity.compareTo(ZERO) > 0 ? remainingQuantity : ZERO;
+
+                // 3.3 查询该物料在所有仓库的库存记录
+                WarehouseMaterial materialQuery = new WarehouseMaterial();
+                materialQuery.setMaterialCode(materialCode);
+                materialQuery.setTenantId(requisition.getTenantId());
+                List<WarehouseMaterial> materials = jdbcClient.getJdbcList(materialQuery, connection);
+
+                // 3.4 按仓库维度组织库存数据
+                List<Map<String, Object>> warehouseList = new ArrayList<>();
+                if (materials != null && !materials.isEmpty()) {
+                    for (WarehouseMaterial wm : materials) {
+                        Map<String, Object> warehouseStock = new java.util.HashMap<>();
+                        warehouseStock.put("warehouseId", wm.getWarehouseId());
+                        // 总库存数量
+                        warehouseStock.put("totalQuantity", wm.getNumber() != null ? wm.getNumber() : ZERO);
+                        // 可用库存数量(未锁定)
+                        warehouseStock.put("normalQuantity", wm.getNormalNumber() != null ? wm.getNormalNumber() : ZERO);
+                        // 已锁定库存数量
+                        warehouseStock.put("lockedQuantity", wm.getLockedNumber() != null ? wm.getLockedNumber() : ZERO);
+
+                        // 3.5 查询该仓库下销售订单可用锁定量
+                        // 销售订单可用锁定量 = 销售订单已锁定量 - 已被其他出库单占用的量
+                        // 这部分库存可以直接复用,无需现场锁定
+                        BigDecimal availableLocked = getSaleOrderAvailableLockedQuantity(
+                            requisition.getSaleOrderId(),
+                            materialCode,
+                            wm.getWarehouseId(),
+                            connection
+                        );
+                        warehouseStock.put("saleOrderAvailableLocked", availableLocked);
+
+                        warehouseList.add(warehouseStock);
+                    }
+                }
+
+                // 3.6 组装物料维度的数据
+                Map<String, Object> materialStock = new java.util.HashMap<>();
+                materialStock.put("materialCode", materialCode);
+                materialStock.put("materialName", item.getMaterialName());
+                materialStock.put("requiredQuantity", requiredQuantity);
+                materialStock.put("outboundedQuantity", outboundedQuantity);
+                materialStock.put("remainingQuantity", remainingQuantity);
+                materialStock.put("unit", item.getUnit());
+                materialStock.put("warehouses", warehouseList);
+
+                result.put(materialCode, materialStock);
+            }
+
+            return result;
+        } catch (Exception e) {
+            throw new BizException(e.getMessage());
+        } finally {
+            connection.close();
+        }
+    }
+
+    /**
+     * 查询领料单信息(用于出库单生成)
+     *
+     * @param requisitionId 领料单ID
+     * @param connection 数据库连接
+     * @return 领料单对象
+     * @throws BizException 领料单ID为空或领料单不存在时抛出异常
+     */
+    private MaterialRequisition getRequisitionForOutbound(String requisitionId, Connection connection) throws Exception {
+        if (requisitionId == null || requisitionId.trim().isEmpty()) {
+            throw new BizException("领料单ID不能为空");
+        }
+
+        MaterialRequisition requisitionQuery = new MaterialRequisition();
+        requisitionQuery.setId(requisitionId);
+        MaterialRequisition requisition = jdbcClient.getJdbcModelById(requisitionQuery, connection);
+        if (requisition == null) {
+            throw new BizException("领料单不存在");
+        }
+
+        return requisition;
+    }
+
+    /**
+     * 创建出库单明细并锁定库存
+     *
+     * 库存锁定策略(两级优先级):
+     * 1. 优先使用销售订单已锁定库存:如果该销售订单下该物料在该仓库已有锁定且未被其他出库单占用,优先复用
+     * 2. 现场锁定新库存:如果销售订单锁定不足,剩余部分从可用库存中现场锁定
+     *
+     * 为什么要优先使用销售订单锁定?
+     * - 避免重复锁定:销售订单下单时已经锁定了库存,领料时可以直接使用
+     * - 提高库存利用率:同一销售订单的多个领料单可以共享已锁定的库存
+     *
+     * @param outbound 出库单主表
+     * @param requisition 领料单
+     * @param outboundItem 出库明细(物料编码、数量、仓库ID)
+     * @param userId 当前用户ID
+     * @param connection 数据库连接
+     */
+    private void createOutboundItemAndLock(WarehouseOutbound outbound,
+                                          MaterialRequisition requisition,
+                                          MaterialRequisitionOutboundVO.OutboundItem outboundItem,
+                                          Long userId,
+                                          Connection connection) throws Exception {
+        BigDecimal required = outboundItem.getQuantity();
+        if (required.compareTo(ZERO) <= 0) {
+            return;
+        }
+
+        String materialCode = outboundItem.getMaterialCode();
+        String warehouseId = outboundItem.getWarehouseId();
+
+        // 1. 查询销售订单在该仓库下该物料的可用锁定量
+        BigDecimal availableLocked = getSaleOrderAvailableLockedQuantity(requisition.getSaleOrderId(), materialCode, warehouseId, connection);
+
+        // 2. 计算使用销售订单锁定的数量(取可用锁定量和需求量的较小值)
+        BigDecimal useSaleLocked = availableLocked.min(required);
+
+        // 3. 计算需要现场锁定的数量
+        BigDecimal remaining = required.subtract(useSaleLocked);
+
+        // 4. 如果有可用的销售订单锁定,记录锁定消耗并创建出库明细(标记为SALE来源)
+        if (useSaleLocked.compareTo(ZERO) > 0) {
+            // 4.1 插入 warehouse_record 记录,标记销售订单锁定已被该领料单占用
+            recordLockConsumption(requisition, materialCode, warehouseId, useSaleLocked, userId, connection);
+            // 4.2 创建出库明细
+            insertOutboundItem(outbound, materialCode, warehouseId, useSaleLocked, MESEnum.WAREHOUSE_OUTBOUND_LOCK_SOURCE_SALE.getValue(), userId, connection);
+        }
+
+        // 5. 如果需要现场锁定,先锁定库存,再创建出库明细(标记为REQUISITION来源)
+        if (remaining.compareTo(ZERO) > 0) {
+            lockWarehouseForRequisition(requisition, materialCode, warehouseId, remaining, userId, connection);
+            insertOutboundItem(outbound, materialCode, warehouseId, remaining, MESEnum.WAREHOUSE_OUTBOUND_LOCK_SOURCE_REQUISITION.getValue(), userId, connection);
+        }
+    }
+
+    /**
+     * 插入出库单明细
+     *
+     * @param outbound 出库单主表
+     * @param materialCode 物料编码
+     * @param warehouseId 仓库ID
+     * @param required 需求数量
+     * @param lockSource 锁定来源(SALE=销售订单锁定,REQUISITION=领料单现场锁定)
+     * @param userId 当前用户ID
+     * @param connection 数据库连接
+     */
+    private void insertOutboundItem(WarehouseOutbound outbound,
+                                    String materialCode,
+                                    String warehouseId,
+                                    BigDecimal required,
+                                    String lockSource,
+                                    Long userId,
+                                    Connection connection) throws Exception {
+        WarehouseOutboundItem outboundItem = new WarehouseOutboundItem();
+        outboundItem.setOutboundId(outbound.getId());
+        outboundItem.setMaterialCode(materialCode);
+        outboundItem.setRequiredQuantity(required);
+        outboundItem.setActualQuantity(ZERO);
+        outboundItem.setLockSource(lockSource);
+        outboundItem.setWarehouseId(warehouseId);
+        outboundItem.setTenantId(outbound.getTenantId());
+        outboundItem.setCreateId(userId);
+        outboundItem.setCreateTime(FORMATTER.format(LocalDateTime.now()));
+        jdbcClient.jdbcInsert(outboundItem, connection);
+    }
+
+    /**
+     * 查询销售订单在指定仓库下指定物料的可用锁定量(基于warehouse_record表)
+     *
+     * 业务逻辑:
+     * 1. 查询该销售订单下该物料在该仓库的所有锁定记录(warehouse_record表)
+     * 2. 只统计"销售订单锁定来源"的记录(排除领料单现场锁定等其他来源)
+     * 3. 查询领料单现场锁定占用的量(warehouse_record表中ref_type=REQUISITION的记录)
+     * 4. 计算可用锁定量 = 销售订单锁定量 - 领料单占用量
+     *
+     * 什么是"销售订单锁定"?
+     * - 在销售订单下单时,系统会预先锁定库存,确保有货可发
+     * - 这部分锁定的库存可以被该销售订单下的多个领料单复用
+     * - 避免重复锁定,提高库存利用率
+     *
+     * 为什么要计算"可用锁定量"?
+     * - 销售订单锁定的库存可能已经被其他领料单占用
+     * - 只有未被占用的部分才能被当前出库单使用
+     * - 例如:销售订单锁定了100个,已有领料单占用了30个,则可用锁定量为70个
+     *
+     * @param saleOrderId 销售订单ID
+     * @param materialCode 物料编码
+     * @param warehouseId 仓库ID
+     * @param connection 数据库连接
+     * @return 可用锁定量(大于等于0)
+     */
+    private BigDecimal getSaleOrderAvailableLockedQuantity(String saleOrderId, String materialCode, String warehouseId, Connection connection) throws Exception {
+        if (saleOrderId == null || saleOrderId.trim().isEmpty()) {
+            return ZERO;
+        }
+
+        // 1. 查询销售订单在该仓库下该物料的所有锁定记录
+        WarehouseRecord saleRecordQuery = new WarehouseRecord();
+        saleRecordQuery.setSaleOrderId(saleOrderId);
+        saleRecordQuery.setMaterialCode(materialCode);
+        saleRecordQuery.setFromWarehouseId(warehouseId);
+        saleRecordQuery.setType(MESEnum.WAREHOUSE_RECORD_OF_TYPE_LOCK.getValue());
+        List<WarehouseRecord> saleRecords = jdbcClient.getJdbcList(saleRecordQuery, connection);
+
+        // 2. 统计销售订单锁定来源的总锁定量(排除领料单现场锁定等其他来源)
+        BigDecimal totalLocked = ZERO;
+        if (saleRecords != null) {
+            for (WarehouseRecord record : saleRecords) {
+                if (!isSaleOrderLockRecord(record)) {
+                    continue;
+                }
+                if (record.getNumber() != null) {
+                    totalLocked = totalLocked.add(record.getNumber());
+                }
+            }
+        }
+
+        if (totalLocked.compareTo(ZERO) == 0) {
+            return ZERO;
+        }
+
+        // 3. 查询领料单现场锁定占用的量(只统计同一销售订单下的领料单锁定)
+        WarehouseRecord requisitionRecordQuery = new WarehouseRecord();
+        requisitionRecordQuery.setSaleOrderId(saleOrderId);
+        requisitionRecordQuery.setMaterialCode(materialCode);
+        requisitionRecordQuery.setFromWarehouseId(warehouseId);
+        requisitionRecordQuery.setType(MESEnum.WAREHOUSE_RECORD_OF_TYPE_LOCK.getValue());
+        requisitionRecordQuery.setRefType(MESEnum.WAREHOUSE_RECORD_OF_REF_TYPE_REQUISITION.getValue());
+        List<WarehouseRecord> requisitionRecords = jdbcClient.getJdbcList(requisitionRecordQuery, connection);
+
+        BigDecimal usedByRequisition = ZERO;
+        if (requisitionRecords != null) {
+            for (WarehouseRecord record : requisitionRecords) {
+                if (record.getNumber() != null) {
+                    usedByRequisition = usedByRequisition.add(record.getNumber());
+                }
+            }
+        }
+
+        // 4. 计算可用锁定量 = 销售订单锁定量 - 领料单占用量
+        BigDecimal available = totalLocked.subtract(usedByRequisition);
+        return available.compareTo(ZERO) > 0 ? available : ZERO;
+    }
+
+    /**
+     * 仅允许消费”销售订单锁定来源”的库存记录,避免误用领料单现场锁定等其它来源
+     */
+    private boolean isSaleOrderLockRecord(WarehouseRecord record) {
+        String refType = record.getRefType();
+        return MESEnum.WAREHOUSE_RECORD_OF_REF_TYPE_PRODUCT.getValue().equals(refType)
+            || MESEnum.WAREHOUSE_RECORD_OF_REF_TYPE_PURCHASE.getValue().equals(refType)
+            || MESEnum.WAREHOUSE_RECORD_OF_REF_TYPE_OUTSOURCING.getValue().equals(refType);
+    }
+
+    /**
+     * 记录销售订单锁定的消耗
+     *
+     * 业务逻辑:
+     * 当领料单使用销售订单已锁定的库存时,需要插入 warehouse_record 记录来标记这部分锁定已被占用。
+     * 这样在统计"销售订单可用锁定量"时,可以准确计算出还有多少锁定量可用。
+     *
+     * 注意:
+     * - 这里只插入记录,不修改 warehouse_material 表(因为预生产计划阶段已经锁定了)
+     * - ref_type 设置为 REQUISITION,表示这是领料单对销售订单锁定的消耗
+     * - sale_order_id 继承自领料单,用于按订单隔离统计
+     *
+     * @param requisition 领料单
+     * @param materialCode 物料编码
+     * @param warehouseId 仓库ID
+     * @param quantity 消耗数量
+     * @param userId 当前用户ID
+     * @param connection 数据库连接
+     */
+    private void recordLockConsumption(MaterialRequisition requisition,
+                                       String materialCode,
+                                       String warehouseId,
+                                       BigDecimal quantity,
+                                       Long userId,
+                                       Connection connection) throws Exception {
+        // 插入 warehouse_record 记录,标记销售订单锁定已被该领料单占用
+        WarehouseRecord record = new WarehouseRecord();
+        record.setType(MESEnum.WAREHOUSE_RECORD_OF_TYPE_LOCK.getValue());
+        record.setRefType(MESEnum.WAREHOUSE_RECORD_OF_REF_TYPE_REQUISITION.getValue());
+        record.setRefId(requisition.getId());
+        record.setSaleOrderId(requisition.getSaleOrderId());
+        record.setMaterialCode(materialCode);
+        record.setFromWarehouseId(warehouseId);
+        record.setNumber(quantity);
+        record.setTenantId(requisition.getTenantId());
+        record.setCreateId(userId);
+        record.setCreateTime(FORMATTER.format(LocalDateTime.now()));
+        jdbcClient.jdbcInsert(record, connection);
+    }
+
+    /**
+     * 现场锁定库存(领料单锁定)
+     *
+     * 业务逻辑:
+     * 1. 创建库存锁定记录(warehouse_record表),记录锁定来源为领料单
+     * 2. 更新库存表(warehouse_material表):
+     *    - normal_number(可用数量)减少
+     *    - locked_number(锁定数量)增加
+     * 3. 如果可用库存不足,抛出异常
+     *
+     * 注意:这是"现场锁定",与"销售订单锁定"不同
+     * - 销售订单锁定:在销售订单下单时就已经锁定,可以被多个领料单复用
+     * - 领料单现场锁定:在生成出库单时临时锁定,仅供当前出库单使用
+     *
+     * @param requisition 领料单
+     * @param materialCode 物料编码
+     * @param warehouseId 仓库ID
+     * @param lockQuantity 锁定数量
+     * @param userId 当前用户ID
+     * @param connection 数据库连接
+     */
+    private void lockWarehouseForRequisition(MaterialRequisition requisition,
+                                             String materialCode,
+                                             String warehouseId,
+                                             BigDecimal lockQuantity,
+                                             Long userId,
+                                             Connection connection) throws Exception {
+        // 1. 创建库存锁定记录
+        WarehouseRecord record = new WarehouseRecord();
+        record.setType(MESEnum.WAREHOUSE_RECORD_OF_TYPE_LOCK.getValue());
+        record.setMaterialCode(materialCode);
+        record.setNumber(lockQuantity);
+        record.setFromWarehouseId(warehouseId);
+        record.setTenantId(requisition.getTenantId());
+        record.setRefType(MESEnum.WAREHOUSE_RECORD_OF_REF_TYPE_REQUISITION.getValue());
+        record.setRefId(requisition.getId());
+        record.setSaleOrderId(requisition.getSaleOrderId());
+        record.setCreateId(userId);
+        record.setCreateTime(FORMATTER.format(LocalDateTime.now()));
+        jdbcClient.jdbcInsert(record, connection);
+
+        // 2. 查询库存记录
+        WarehouseMaterial wm = new WarehouseMaterial();
+        wm.setWarehouseId(warehouseId);
+        wm.setMaterialCode(materialCode);
+        wm = jdbcClient.getJdbcModel(wm, connection);
+        if (wm == null) {
+            throw new BizException("库存记录不存在: " + materialCode);
+        }
+
+        // 3. 验证可用库存是否充足
+        BigDecimal currentNormal = wm.getNormalNumber() != null ? wm.getNormalNumber() : ZERO;
+        if (currentNormal.compareTo(lockQuantity) < 0) {
+            throw new BizException("库存不足: " + materialCode);
+        }
+
+        // 4. 更新库存:扣减可用数量,增加锁定数量
+        wm.setNormalNumber(currentNormal.subtract(lockQuantity));
+        wm.setLockedNumber((wm.getLockedNumber() != null ? wm.getLockedNumber() : ZERO).add(lockQuantity));
+        wm.setUpdateId(userId);
+        wm.setUpdateTime(FORMATTER.format(LocalDateTime.now()));
+        jdbcClient.jdbcUpdateById(wm, connection);
+    }
+
 }

+ 31 - 54
easydo-mes/src/main/java/easydo/technology/service/impl/ProductOrderDispatchServiceImpl.java

@@ -10,6 +10,7 @@ import easydo.technology.service.FlowNoService;
 import easydo.technology.service.MaterialRequisitionService;
 import easydo.technology.service.ProductOrderDispatchService;
 import easydo.technology.system.model.SysUser;
+import easydo.technology.utils.ConvertUtil;
 import easydo.technology.utils.SecurityUtils;
 import org.springframework.stereotype.Service;
 
@@ -108,32 +109,8 @@ public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchServ
             // 转换为 VO 对象
             List<ProductOrderDispatchVO> voList = new ArrayList<>();
             for (ProductOrderDispatch model : list) {
-                ProductOrderDispatchVO vo = new ProductOrderDispatchVO();
-                
-                // 复制主要字段
-                vo.setId(model.getId());
-                vo.setCode(model.getCode());
-                vo.setName(model.getName());
-                vo.setOrderId(model.getOrderId());
-                vo.setBomId(model.getBomId());
-                vo.setStageId(model.getStageId());
-                vo.setRouteId(model.getRouteId());
-                vo.setOrderNum(model.getOrderNum());
-                vo.setProcessNum(model.getProcessNum());
-                vo.setProcessTimeHour(model.getProcessTimeHour());
-                vo.setReadyTimeHour(model.getReadyTimeHour());
-                vo.setMoveNum(model.getMoveNum());
-                vo.setMoveTimeHour(model.getMoveTimeHour());
-                vo.setStatus(model.getStatus());
-                vo.setIsReport(model.getIsReport());
-                vo.setIsReview(model.getIsReview());
-                vo.setIsRound(model.getIsRound());
-                vo.setIsInspection(model.getIsInspection());
-                vo.setBeginDate(model.getBeginDate());
-                vo.setEndDate(model.getEndDate());
-                vo.setCreateTime(model.getCreateTime());
-                vo.setTenantId(model.getTenantId());
-                
+                ProductOrderDispatchVO vo = ConvertUtil.convert(model, ProductOrderDispatchVO.class);
+
                 // 查询并设置工单信息(返回整表对象)
                 if (model.getOrderId() != null) {
                     ProductOrder orderParam = new ProductOrder();
@@ -186,12 +163,7 @@ public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchServ
                 if (userItems != null && !userItems.isEmpty()) {
                     List<ProductOrderDispatchVO.SimpleDispatchUserItem> simpleUserItems = new ArrayList<>();
                     for (DispatchUserItem item : userItems) {
-                        ProductOrderDispatchVO.SimpleDispatchUserItem simpleItem = new ProductOrderDispatchVO.SimpleDispatchUserItem();
-                        simpleItem.setId(item.getId());
-                        simpleItem.setUserId(item.getUserId());
-                        simpleItem.setStatus(item.getStatus());
-                        simpleItem.setIsReport(item.getIsReport());
-                        simpleItem.setIsReview(item.getIsReview());
+                        ProductOrderDispatchVO.SimpleDispatchUserItem simpleItem = ConvertUtil.convert(item, ProductOrderDispatchVO.SimpleDispatchUserItem.class);
                         
                         // 查询用户信息
                         if (item.getUserId() != null) {
@@ -199,10 +171,7 @@ public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchServ
                             userParam.setId(item.getUserId());
                             SysUser user = jdbcClient.getJdbcModelById(userParam, connection);
                             if (user != null) {
-                                ProductOrderDispatchVO.SimpleUser simpleUser = new ProductOrderDispatchVO.SimpleUser();
-                                simpleUser.setId(user.getId());
-                                simpleUser.setUsername(user.getUsername());
-                                simpleUser.setNickName(user.getNickName());
+                                ProductOrderDispatchVO.SimpleUser simpleUser = ConvertUtil.convert(user, ProductOrderDispatchVO.SimpleUser.class);
                                 simpleItem.setUser(simpleUser);
                             }
                         }
@@ -250,6 +219,7 @@ public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchServ
 
             // 2. 获取租户ID
             String tenantId = order.getTenantId();
+            String saleOrderId = order.getSaleOrderId();
 
             // 3. 按明细处理:新增 / 修改 / 删除
             if (vo.getItems() != null && !vo.getItems().isEmpty()) {
@@ -280,12 +250,32 @@ public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchServ
                             throw new BizException("派工记录与工单不匹配: " + item.getId());
                         }
 
-                        // 先删子表
+                        // 先删派工人员子表
                         DispatchUserItem removeUserItemParam = new DispatchUserItem();
                         removeUserItemParam.setDispatchId(item.getId());
                         jdbcClient.jdbcRemove(removeUserItemParam, connection);
 
-                        // 再删主表
+                        // 删除关联领料单明细与主单(按 dispatch_id 关联)
+                        MaterialRequisition requisitionQuery = new MaterialRequisition();
+                        requisitionQuery.setDispatchId(item.getId());
+                        List<MaterialRequisition> requisitionList = jdbcClient.getJdbcList(requisitionQuery, connection);
+                        if (requisitionList != null && !requisitionList.isEmpty()) {
+                            for (MaterialRequisition requisition : requisitionList) {
+                                if (requisition.getId() == null || requisition.getId().trim().isEmpty()) {
+                                    continue;
+                                }
+
+                                MaterialRequisitionItem requisitionItemParam = new MaterialRequisitionItem();
+                                requisitionItemParam.setRequisitionId(requisition.getId());
+                                jdbcClient.jdbcRemove(requisitionItemParam, connection);
+
+                                MaterialRequisition removeRequisitionParam = new MaterialRequisition();
+                                removeRequisitionParam.setId(requisition.getId());
+                                jdbcClient.jdbcRemoveById(removeRequisitionParam, connection);
+                            }
+                        }
+
+                        // 再删派工主表
                         ProductOrderDispatch removeDispatchParam = new ProductOrderDispatch();
                         removeDispatchParam.setId(item.getId());
                         jdbcClient.jdbcRemoveById(removeDispatchParam, connection);
@@ -343,6 +333,7 @@ public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchServ
                         existDispatch.setName(item.getName());
                         existDispatch.setRouteId(routeId);
                         existDispatch.setBomId(item.getBomId());
+                        existDispatch.setSaleOrderId(saleOrderId);
                         existDispatch.setStageId(item.getStageId());
                         existDispatch.setOrderNum(item.getOrderNum());
                         existDispatch.setProcessNum(item.getProcessNum());
@@ -395,6 +386,7 @@ public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchServ
                         dispatch.setName(item.getName());
                         dispatch.setRouteId(routeId);
                         dispatch.setBomId(item.getBomId());
+                        dispatch.setSaleOrderId(saleOrderId);
                         dispatch.setStageId(item.getStageId());
                         dispatch.setOrderNum(item.getOrderNum());
                         dispatch.setProcessNum(item.getProcessNum());
@@ -596,22 +588,7 @@ public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchServ
                     List<ProductOrderDispatchVO.SimpleMaterialRequisition> simpleRequisitions = new ArrayList<>();
                     for (MaterialRequisition req : requisitions) {
                         ProductOrderDispatchVO.SimpleMaterialRequisition simpleReq = 
-                            new ProductOrderDispatchVO.SimpleMaterialRequisition();
-                        simpleReq.setId(req.getId());
-                        simpleReq.setCode(req.getCode());
-                        simpleReq.setDispatchId(req.getDispatchId());
-                        simpleReq.setOrderId(req.getOrderId());
-                        simpleReq.setBomId(req.getBomId());
-                        simpleReq.setRequisitionType(req.getRequisitionType());
-                        simpleReq.setOutboundStatus(req.getOutboundStatus());
-                        simpleReq.setStatus(req.getStatus());
-                        simpleReq.setRequisitionDate(req.getRequisitionDate());
-                        simpleReq.setRemark(req.getRemark());
-                        simpleReq.setTenantId(req.getTenantId());
-                        simpleReq.setCreateId(req.getCreateId());
-                        simpleReq.setCreateTime(req.getCreateTime());
-                        simpleReq.setUpdateId(req.getUpdateId());
-                        simpleReq.setUpdateTime(req.getUpdateTime());
+                            ConvertUtil.convert(req, ProductOrderDispatchVO.SimpleMaterialRequisition.class);
                         simpleRequisitions.add(simpleReq);
                     }
                     vo.setMaterialRequisitions(simpleRequisitions);

+ 2 - 2
easydo-mes/src/main/java/easydo/technology/service/impl/ProductPrePlanServiceImpl.java

@@ -347,8 +347,8 @@ public class ProductPrePlanServiceImpl implements ProductPrePlanService {
             wm.setMaterialCode(materialCode);
             wm = jdbcClient.getJdbcModel(wm, connection);
             if (wm != null) {
-                // 可用库存 number 扣减,锁定库存 lockedNumber 增加
-                wm.setNumber(wm.getNumber() == null ? null : wm.getNumber().subtract(wmVo.getLockedNumber()));
+                // 可用库存 normalNumber 扣减,锁定库存 lockedNumber 增加
+                wm.setNormalNumber(wm.getNormalNumber() == null ? null : wm.getNormalNumber().subtract(wmVo.getLockedNumber()));
                 wm.setLockedNumber((wm.getLockedNumber() != null ? wm.getLockedNumber() : java.math.BigDecimal.ZERO)
                         .add(wmVo.getLockedNumber()));
                 wm.setUpdateId(userId);

+ 109 - 0
easydo-mes/src/main/java/easydo/technology/service/impl/WarehouseMaterialServiceImpl.java

@@ -0,0 +1,109 @@
+package easydo.technology.service.impl;
+
+import easydo.technology.components.JdbcClient;
+import easydo.technology.model.ProductMaterial;
+import easydo.technology.model.Warehouse;
+import easydo.technology.model.WarehouseMaterial;
+import easydo.technology.service.WarehouseMaterialService;
+import easydo.technology.utils.MapUtil;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@Service
+public class WarehouseMaterialServiceImpl implements WarehouseMaterialService {
+
+    @Resource
+    private JdbcClient jdbcClient;
+
+    @Resource
+    private DataSource dataSource;
+
+    @Override
+    public Map<String, Object> getPage(Map<String, Object> map) throws Exception {
+        Connection connection = dataSource.getConnection();
+        try {
+            Map<String, Object> page = jdbcClient.getJdbcPage(map, WarehouseMaterial.class, connection);
+            List<WarehouseMaterial> records = MapUtil.mapToList(page, WarehouseMaterial.class, "records");
+            List<Map<String, Object>> resultRecords = buildMaterialRecords(records, connection);
+            page.put("records", resultRecords);
+            return page;
+        } finally {
+            connection.close();
+        }
+    }
+
+    @Override
+    public Object getList(Map<String, Object> map) throws Exception {
+        Connection connection = dataSource.getConnection();
+        try {
+            List<WarehouseMaterial> list = jdbcClient.getJdbcList(map, WarehouseMaterial.class, connection);
+            return buildMaterialRecords(list, connection);
+        } finally {
+            connection.close();
+        }
+    }
+
+    private List<Map<String, Object>> buildMaterialRecords(List<WarehouseMaterial> records, Connection connection) throws Exception {
+        List<Map<String, Object>> resultRecords = new ArrayList<>();
+        if (records == null || records.isEmpty()) {
+            return resultRecords;
+        }
+
+        Set<String> materialCodes = new HashSet<>();
+        Set<String> warehouseIds = new HashSet<>();
+        for (WarehouseMaterial record : records) {
+            if (record.getMaterialCode() != null && !record.getMaterialCode().trim().isEmpty()) {
+                materialCodes.add(record.getMaterialCode());
+            }
+            if (record.getWarehouseId() != null && !record.getWarehouseId().trim().isEmpty()) {
+                warehouseIds.add(record.getWarehouseId());
+            }
+        }
+
+        Map<String, ProductMaterial> materialMap = new HashMap<>();
+        if (!materialCodes.isEmpty()) {
+            Map<String, Object> materialMapParam = new HashMap<>();
+            materialMapParam.put("code_in", new ArrayList<>(materialCodes));
+            List<ProductMaterial> materials = jdbcClient.getJdbcList(materialMapParam, ProductMaterial.class, connection);
+            if (materials != null) {
+                for (ProductMaterial material : materials) {
+                    materialMap.put(material.getCode(), material);
+                }
+            }
+        }
+
+        Map<String, Warehouse> warehouseMap = new HashMap<>();
+        if (!warehouseIds.isEmpty()) {
+            Map<String, Object> warehouseMapParam = new HashMap<>();
+            warehouseMapParam.put("id_in", new ArrayList<>(warehouseIds));
+            List<Warehouse> warehouses = jdbcClient.getJdbcList(warehouseMapParam, Warehouse.class, connection);
+            if (warehouses != null) {
+                for (Warehouse warehouse : warehouses) {
+                    warehouseMap.put(warehouse.getId(), warehouse);
+                }
+            }
+        }
+
+        for (WarehouseMaterial record : records) {
+            Map<String, Object> recordMap = MapUtil.objectToMap(record);
+            ProductMaterial material = materialMap.get(record.getMaterialCode());
+            Warehouse warehouse = warehouseMap.get(record.getWarehouseId());
+            recordMap.put("specification", material != null ? material.getSpecification() : "");
+            recordMap.put("materialName", material != null ? material.getName() : "");
+            recordMap.put("unit", material != null ? material.getUnit() : "");
+            recordMap.put("warehouseName", warehouse != null ? warehouse.getName() : "");
+            resultRecords.add(recordMap);
+        }
+
+        return resultRecords;
+    }
+}