lumaojun hai 2 semanas
pai
achega
adf8c99d3f

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

@@ -0,0 +1,34 @@
+package easydo.technology.controller;
+
+import easydo.technology.service.MaterialRequisitionService;
+import easydo.technology.utils.SecurityUtils;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+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.Map;
+
+/**
+ * 领料单控制器
+ */
+@RestController
+@RequestMapping("/materialRequisition")
+public class MaterialRequisitionController {
+
+    @Resource
+    private MaterialRequisitionService materialRequisitionService;
+
+    /**
+     * 分页查询领料单
+     */
+    @RequestMapping(value = "/getPage")
+    public Object getPage(@RequestBody Map<String, Object> map) throws Exception {
+        // 校验用户登录状态
+        Long userId = SecurityUtils.getCurrentUserId();
+        Map<String, Object> result = materialRequisitionService.getPage(map);
+        return new ResponseEntity<>(result, HttpStatus.OK);
+    }
+}

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

@@ -66,6 +66,7 @@ public enum MESEnum {
     FLOW_NO_TYPE_PRODUCT_PLAN("product_plan", "生产计划"),
     FLOW_NO_TYPE_PRODUCT_ORDER("product_order", "工单"),
     FLOW_NO_TYPE_PRODUCT_ORDER_DISPATCH("product_order_dispatch", "派工单"),
+    FLOW_NO_TYPE_MATERIAL_REQUISITION("material_requisition", "领料单"),
 
     PRODUCT_PLAN_OF_STATUS_PENDING("pending", "待进行"),
     PRODUCT_PLAN_OF_STATUS_PROCESSING("processing", "进行中"),
@@ -80,12 +81,27 @@ public enum MESEnum {
     PRODUCT_ORDER_OF_PRIORITY_MEDIUM("medium", "中"),
     PRODUCT_ORDER_OF_PRIORITY_LOW("low", "低"),
 
+    // 派工单领料状态
+    DISPATCH_REQUISITION_STATUS_PENDING("pending", "未领料"),
+    DISPATCH_REQUISITION_STATUS_PARTIALLY("partially", "部分领料"),
+    DISPATCH_REQUISITION_STATUS_COMPLETE("complete", "已领料"),
+    
     // 派工单状态
     PRODUCT_ORDER_DISPATCH_OF_STATUS_PENDING("pending", "未开始"),
     PRODUCT_ORDER_DISPATCH_OF_STATUS_PROCESSING("processing", "生产中"),
     PRODUCT_ORDER_DISPATCH_OF_STATUS_COMPLETED("completed", "已完工"),
     PRODUCT_ORDER_DISPATCH_OF_STATUS_REWORK("rework", "待返工"),
     
+    // 领料单类型
+    MATERIAL_REQUISITION_TYPE_AUTO("auto", "领料(自动生成)"),
+    MATERIAL_REQUISITION_TYPE_MANUAL("manual", "补料(手动新增)"),
+    
+    // 领料单出库状态
+    MATERIAL_REQUISITION_OUTBOUND_STATUS_APPLIED("applied", "申请完毕"),
+    MATERIAL_REQUISITION_OUTBOUND_STATUS_PENDING("pending", "未出库"),
+    MATERIAL_REQUISITION_OUTBOUND_STATUS_PARTIAL("partially", "部分出库"),
+    MATERIAL_REQUISITION_OUTBOUND_STATUS_COMPLETED("completed", "出库完毕"),
+    
     // 领料单状态
     MATERIAL_REQUISITION_OF_STATUS_PENDING("pending", "未领料"),
     MATERIAL_REQUISITION_OF_STATUS_COMPLETE("complete", "已领料"),

+ 143 - 0
easydo-mes/src/main/java/easydo/technology/model/MaterialRequisition.java

@@ -0,0 +1,143 @@
+package easydo.technology.model;
+
+/**
+ * 领料单主表
+ */
+public class MaterialRequisition {
+    
+    private String id;
+    private String code;
+    private String dispatchId;
+    private String orderId;
+    private String bomId;
+    private String requisitionType;
+    private String outboundStatus;
+    private String status;
+    private String requisitionDate;
+    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;
+    }
+}

+ 109 - 0
easydo-mes/src/main/java/easydo/technology/model/MaterialRequisitionItem.java

@@ -0,0 +1,109 @@
+package easydo.technology.model;
+
+import java.math.BigDecimal;
+
+/**
+ * 领料单明细表
+ */
+public class MaterialRequisitionItem {
+    
+    private String id;
+    private String requisitionId;
+    private String materialCode;
+    private String materialName;
+    private BigDecimal requiredQuantity;
+    private BigDecimal actualQuantity;
+    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;
+    }
+}

+ 9 - 0
easydo-mes/src/main/java/easydo/technology/model/vo/DispatchVO.java

@@ -2,6 +2,7 @@ package easydo.technology.model.vo;
 
 import lombok.Data;
 
+import java.math.BigDecimal;
 import java.util.List;
 
 /**
@@ -15,6 +16,8 @@ public class DispatchVO {
     @Data
     public static class DispatchItem {
         private String id;               // 派工ID(有值=修改/删除,无值=新增)
+        private String code;             // 派工编号
+        private String name;             // 派工名称
         private String bomId;            // BOM ID(必填)
         private String stageId;          // 工序ID
         private List<Long> userIds;      // 操作工ID列表(支持多个用户)
@@ -22,5 +25,11 @@ public class DispatchVO {
         private Boolean isInspection;    // 是否质检
         private String beginDate;        // 计划开始日期
         private String endDate;          // 计划结束日期
+
+        private BigDecimal processNum;
+        private BigDecimal processTimeHour;
+        private BigDecimal readyTimeHour;
+        private BigDecimal moveNum;
+        private BigDecimal moveTimeHour;
     }
 }

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

@@ -0,0 +1,32 @@
+package easydo.technology.model.vo;
+
+import easydo.technology.model.MaterialRequisitionItem;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 领料单 VO(包含明细列表)
+ */
+@Data
+public class MaterialRequisitionVO {
+    
+    private String id;
+    private String code;
+    private String dispatchId;
+    private String orderId;
+    private String bomId;
+    private String requisitionType;
+    private String outboundStatus;
+    private String status;
+    private String requisitionDate;
+    private String remark;
+    private String tenantId;
+    private Long createId;
+    private String createTime;
+    private Long updateId;
+    private String updateTime;
+    
+    // 关联信息
+    private List<MaterialRequisitionItem> items;  // 领料单明细列表
+}

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

@@ -38,12 +38,16 @@ public class ProductOrderDispatchVO {
     private String createTime;
     private String tenantId;
     
+    // 领料状态(计算字段)
+    private String requisitionStatus;  // pending-未领料, partially-部分领料, complete-已领料
+    
     // 关联对象(返回整表对象)
     private ProductOrder productOrder;
     private ProductOrderBom productOrderBom;
     private ProductBom productBom;
     private ProcessStage stage;
     private List<SimpleDispatchUserItem> userItems;
+    private List<SimpleMaterialRequisition> materialRequisitions;  // 领料单列表
     
     @Data
     public static class SimpleDispatchUserItem {
@@ -59,5 +63,25 @@ public class ProductOrderDispatchVO {
     public static class SimpleUser {
         private Long id;
         private String username;
+        private String nickName;
+    }
+    
+    @Data
+    public static class SimpleMaterialRequisition {
+        private String id;
+        private String code;
+        private String dispatchId;
+        private String orderId;
+        private String bomId;
+        private String requisitionType;  // auto-领料, manual-补料
+        private String outboundStatus;   // applied-申请完毕, pending-未出库, partial-部分出库, completed-出库完毕
+        private String status;           // pending-未领料, complete-已领料
+        private String requisitionDate;
+        private String remark;
+        private String tenantId;
+        private Long createId;
+        private String createTime;
+        private Long updateId;
+        private String updateTime;
     }
 }

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

@@ -40,6 +40,7 @@ public class FlowNoService {
     private final Object purchaseOrderLock = new Object();
     private final Object productOrderLock = new Object();
     private final Object productOrderDispatchLock = new Object();
+    private final Object materialRequisitionLock = new Object();
 
 
     /**
@@ -818,6 +819,62 @@ public class FlowNoService {
         }
     }
 
+    /**
+     * 生成领料单编码 (MaterialRequisition) - 独立解耦实现
+     */
+    public String generateMaterialRequisitionCode(MaterialRequisition model, Connection connection) throws Exception {
+        synchronized (materialRequisitionLock) {
+            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, MaterialRequisition.class, connection);
+                if (count > 0) {
+                    throw new BizException("领料单编号已存在: " + manualCode);
+                }
+                return manualCode;
+            }
+
+            while (true) {
+                FlowNo flowNo = new FlowNo();
+                flowNo.setType(MESEnum.FLOW_NO_TYPE_MATERIAL_REQUISITION.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_MATERIAL_REQUISITION.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, MaterialRequisition.class, connection);
+                if (count == 0) return no;
+            }
+        }
+    }
+
     /**
      * 原始流水号生成方法
      */

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

@@ -0,0 +1,34 @@
+package easydo.technology.service;
+
+import easydo.technology.model.ProductOrderDispatch;
+
+import java.sql.Connection;
+import java.util.Map;
+
+/**
+ * 领料单服务接口
+ */
+public interface MaterialRequisitionService {
+    
+    /**
+     * 分页查询领料单
+     * @param map 查询参数
+     * @return 分页结果
+     */
+    Map<String, Object> getPage(Map<String, Object> map) throws Exception;
+    
+    /**
+     * 生成领料单(在派工时调用)
+     * @param dispatch 派工单信息
+     * @param connection 外部事务的 connection
+     */
+    void generateRequisition(ProductOrderDispatch dispatch, Connection connection) throws Exception;
+    
+    /**
+     * 更新领料单状态
+     * @param requisitionId 领料单ID
+     * @param status 新状态
+     * @param connection 外部事务的 connection
+     */
+    void updateStatus(String requisitionId, String status, Connection connection) throws Exception;
+}

+ 223 - 0
easydo-mes/src/main/java/easydo/technology/service/impl/MaterialRequisitionServiceImpl.java

@@ -0,0 +1,223 @@
+package easydo.technology.service.impl;
+
+import easydo.technology.components.JdbcClient;
+import easydo.technology.enums.MESEnum;
+import easydo.technology.exception.BizException;
+import easydo.technology.model.*;
+import easydo.technology.model.vo.MaterialRequisitionVO;
+import easydo.technology.service.FlowNoService;
+import easydo.technology.service.MaterialRequisitionService;
+import easydo.technology.utils.SecurityUtils;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import javax.sql.DataSource;
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class MaterialRequisitionServiceImpl implements MaterialRequisitionService {
+
+    @Resource
+    private JdbcClient jdbcClient;
+    @Resource
+    private DataSource dataSource;
+    @Resource
+    private FlowNoService flowNoService;
+
+    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public Map<String, Object> getPage(Map<String, Object> map) throws Exception {
+        Connection connection = dataSource.getConnection();
+        try {
+            // 查询领料单分页数据
+            Map<String, Object> result = jdbcClient.getJdbcPage(map, MaterialRequisition.class, connection);
+            List<MaterialRequisition> list = (List<MaterialRequisition>) result.get("records");
+            
+            if (list == null || list.isEmpty()) {
+                return result;
+            }
+
+            // 转换为 VO 对象,并查询明细
+            List<MaterialRequisitionVO> voList = new ArrayList<>();
+            for (MaterialRequisition model : list) {
+                MaterialRequisitionVO vo = new MaterialRequisitionVO();
+                
+                // 复制主要字段
+                vo.setId(model.getId());
+                vo.setCode(model.getCode());
+                vo.setDispatchId(model.getDispatchId());
+                vo.setOrderId(model.getOrderId());
+                vo.setBomId(model.getBomId());
+                vo.setRequisitionType(model.getRequisitionType());
+                vo.setOutboundStatus(model.getOutboundStatus());
+                vo.setStatus(model.getStatus());
+                vo.setRequisitionDate(model.getRequisitionDate());
+                vo.setRemark(model.getRemark());
+                vo.setTenantId(model.getTenantId());
+                vo.setCreateId(model.getCreateId());
+                vo.setCreateTime(model.getCreateTime());
+                vo.setUpdateId(model.getUpdateId());
+                vo.setUpdateTime(model.getUpdateTime());
+                
+                // 查询领料单明细
+                MaterialRequisitionItem itemParam = new MaterialRequisitionItem();
+                itemParam.setRequisitionId(model.getId());
+                List<MaterialRequisitionItem> items = jdbcClient.getJdbcList(itemParam, connection);
+                vo.setItems(items);
+                
+                voList.add(vo);
+            }
+            
+            // 替换 records
+            result.put("records", voList);
+            return result;
+        } catch (Exception e) {
+            throw new BizException(e.getMessage());
+        } finally {
+            connection.close();
+        }
+    }
+
+    @Override
+    public void generateRequisition(ProductOrderDispatch dispatch, Connection connection) throws Exception {
+        try {
+            // 1. 检查是否首道工序(order_num 最小的工序)
+            // 先查询该工艺路线的所有工序,找出最小的 order_num
+            ProcessRouteDetail allRouteQuery = new ProcessRouteDetail();
+            allRouteQuery.setRouteId(dispatch.getRouteId());
+            List<ProcessRouteDetail> allRouteDetails = jdbcClient.getJdbcList(allRouteQuery, connection);
+            
+            if (allRouteDetails == null || allRouteDetails.isEmpty()) {
+                // 找不到工序信息,不生成领料单
+                return;
+            }
+            
+            // 找出最小的 order_num
+            Integer minOrderNum = allRouteDetails.stream()
+                .map(ProcessRouteDetail::getOrderNum)
+                .filter(num -> num != null)
+                .min(Integer::compareTo)
+                .orElse(null);
+            
+            if (minOrderNum == null) {
+                // 没有有效的 order_num,不生成领料单
+                return;
+            }
+            
+            // 查询当前派工的工序信息
+            ProcessRouteDetail currentRouteQuery = new ProcessRouteDetail();
+            currentRouteQuery.setRouteId(dispatch.getRouteId());
+            currentRouteQuery.setStageId(dispatch.getStageId());
+            List<ProcessRouteDetail> currentRouteDetails = jdbcClient.getJdbcList(currentRouteQuery, connection);
+            
+            if (currentRouteDetails == null || currentRouteDetails.isEmpty()) {
+                // 找不到当前工序信息,不生成领料单
+                return;
+            }
+            
+            ProcessRouteDetail currentRouteDetail = currentRouteDetails.get(0);
+            if (currentRouteDetail.getOrderNum() == null || !currentRouteDetail.getOrderNum().equals(minOrderNum)) {
+                // 非首道工序,不生成领料单
+                return;
+            }
+            
+            // 2. 查询次级 BOM(只查一层,parent_id = bomId)
+            ProductBom subBomQuery = new ProductBom();
+            subBomQuery.setParentId(dispatch.getBomId());
+            subBomQuery.setTenantId(dispatch.getTenantId());
+            List<ProductBom> subBoms = jdbcClient.getJdbcList(subBomQuery, connection);
+            
+            if (subBoms == null || subBoms.isEmpty()) {
+                // 无次级 BOM,不生成领料单
+                return;
+            }
+            
+            // 3. 创建领料单主表
+            MaterialRequisition requisition = new MaterialRequisition();
+            requisition.setDispatchId(dispatch.getId());
+            requisition.setOrderId(dispatch.getOrderId());
+            requisition.setBomId(dispatch.getBomId());
+            requisition.setRequisitionType(MESEnum.MATERIAL_REQUISITION_TYPE_AUTO.getValue());  // 自动生成
+            requisition.setOutboundStatus(MESEnum.MATERIAL_REQUISITION_OUTBOUND_STATUS_APPLIED.getValue());  // 申请完毕
+            requisition.setStatus(MESEnum.MATERIAL_REQUISITION_OF_STATUS_PENDING.getValue());
+            // requisition_date 领料日期,在实际领料时填入,生成时不填
+            requisition.setTenantId(dispatch.getTenantId());
+            requisition.setCreateId(dispatch.getCreateId());
+            requisition.setCreateTime(FORMATTER.format(LocalDateTime.now()));
+            
+            // 生成领料单编号
+            String requisitionCode = flowNoService.generateMaterialRequisitionCode(requisition, connection);
+            requisition.setCode(requisitionCode);
+            
+            jdbcClient.jdbcInsert(requisition, connection);
+            
+            // 4. 创建领料单明细(每个次级 BOM 一条明细)
+            for (ProductBom subBom : subBoms) {
+                MaterialRequisitionItem item = new MaterialRequisitionItem();
+                item.setRequisitionId(requisition.getId());
+                item.setMaterialCode(subBom.getMaterialCode());
+                item.setMaterialName(subBom.getMaterialName());
+                
+                // 计算需求数量:派工数量 × 次级BOM用量
+                BigDecimal requiredQuantity = new BigDecimal(dispatch.getOrderNum())
+                    .multiply(subBom.getQuantity() != null ? subBom.getQuantity() : BigDecimal.ZERO);
+                item.setRequiredQuantity(requiredQuantity);
+                
+                item.setActualQuantity(BigDecimal.ZERO);
+                
+                // 查询物料信息获取单位
+                String unit = null;
+                if (subBom.getMaterialCode() != null) {
+                    ProductMaterial materialQuery = new ProductMaterial();
+                    materialQuery.setCode(subBom.getMaterialCode());
+                    materialQuery.setTenantId(dispatch.getTenantId());
+                    List<ProductMaterial> materials = jdbcClient.getJdbcList(materialQuery, connection);
+                    if (materials != null && !materials.isEmpty()) {
+                        unit = materials.get(0).getUnit();
+                    }
+                }
+                item.setUnit(unit);
+                
+                item.setTenantId(dispatch.getTenantId());
+                item.setCreateTime(FORMATTER.format(LocalDateTime.now()));
+                
+                jdbcClient.jdbcInsert(item, connection);
+            }
+            
+        } catch (Exception e) {
+            throw new BizException("生成领料单失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public void updateStatus(String requisitionId, String status, Connection connection) throws Exception {
+        try {
+            MaterialRequisition requisition = new MaterialRequisition();
+            requisition.setId(requisitionId);
+            requisition = jdbcClient.getJdbcModelById(requisition, connection);
+            
+            if (requisition == null) {
+                throw new BizException("领料单不存在");
+            }
+            
+            Long userId = SecurityUtils.getCurrentUserId();
+            requisition.setStatus(status);
+            requisition.setUpdateId(userId);
+            requisition.setUpdateTime(FORMATTER.format(LocalDateTime.now()));
+            
+            jdbcClient.jdbcUpdateById(requisition, connection);
+            
+        } catch (Exception e) {
+            throw new BizException("更新领料单状态失败: " + e.getMessage());
+        }
+    }
+}

+ 281 - 1
easydo-mes/src/main/java/easydo/technology/service/impl/ProductOrderDispatchServiceImpl.java

@@ -7,6 +7,7 @@ import easydo.technology.model.*;
 import easydo.technology.model.vo.DispatchVO;
 import easydo.technology.model.vo.ProductOrderDispatchVO;
 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.SecurityUtils;
@@ -22,6 +23,61 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+/**
+ * 派工单服务实现类
+ * 
+ * ============================================
+ * getPage() 接口表关联关系图
+ * ============================================
+ * 
+ * 主表:product_order_dispatch(派工单)
+ * │
+ * ├─→ product_order(工单)
+ * │   关联:dispatch.order_id = order.id
+ * │
+ * ├─→ process_stage(工序)
+ * │   关联:dispatch.stage_id = stage.id
+ * │
+ * ├─→ product_order_bom(工单BOM)
+ * │   关联:dispatch.order_id = order_bom.order_id 
+ * │          AND dispatch.bom_id = order_bom.bom_id
+ * │   │
+ * │   └─→ product_bom(产品BOM)
+ * │       关联:order_bom.bom_id = product_bom.id
+ * │
+ * ├─→ dispatch_user_item(派工人员)
+ * │   关联:dispatch.id = user_item.dispatch_id
+ * │   │
+ * │   └─→ sys_user(用户)
+ * │       关联:user_item.user_id = user.id
+ * │
+ * └─→ material_requisition(领料单)
+ *     关联:dispatch.order_id = requisition.order_id
+ *            AND dispatch.bom_id = requisition.bom_id
+ * 
+ * ============================================
+ * 查询优化说明
+ * ============================================
+ * 
+ * 当前实现:单表查询 + 应用层拼接
+ * - 优点:逻辑清晰,易于维护
+ * - 缺点:查询次数多(N+1问题)
+ * 
+ * 查询次数统计(假设返回10条记录):
+ * - 主查询:1次
+ * - 工单/工序/BOM:各10次
+ * - 派工人员:10次
+ * - 用户信息:30次(假设每个派工3人)
+ * - 领料单:5次(去重优化后)
+ * - 总计:约86次数据库查询
+ * 
+ * 优化方向:
+ * 1. 主查询改用连表SQL(减少到1次)
+ * 2. 子查询改用批量IN查询(3-4次)
+ * 3. 总查询次数可降至4-5次
+ * 
+ * ============================================
+ */
 @Service
 public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchService {
 
@@ -31,6 +87,8 @@ public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchServ
     private DataSource dataSource;
     @Resource
     private FlowNoService flowNoService;
+    @Resource
+    private MaterialRequisitionService materialRequisitionService;
 
     private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
 
@@ -144,6 +202,7 @@ public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchServ
                                 ProductOrderDispatchVO.SimpleUser simpleUser = new ProductOrderDispatchVO.SimpleUser();
                                 simpleUser.setId(user.getId());
                                 simpleUser.setUsername(user.getUsername());
+                                simpleUser.setNickName(user.getNickName());
                                 simpleItem.setUser(simpleUser);
                             }
                         }
@@ -156,6 +215,11 @@ public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchServ
                 voList.add(vo);
             }
             
+            // 批量查询所有派工单的领料单状态并设置领料状态
+            if (!voList.isEmpty()) {
+                calculateRequisitionStatus(voList, connection);
+            }
+            
             // 替换 records
             result.put("records", voList);
             return result;
@@ -196,6 +260,11 @@ public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchServ
                             && (item.getStageId() == null || item.getStageId().trim().isEmpty())
                             && (item.getUserIds() == null || item.getUserIds().isEmpty())
                             && item.getOrderNum() == null
+                            && item.getProcessNum() == null
+                            && item.getProcessTimeHour() == null
+                            && item.getReadyTimeHour() == null
+                            && item.getMoveNum() == null
+                            && item.getMoveTimeHour() == null
                             && (item.getBeginDate() == null || item.getBeginDate().trim().isEmpty())
                             && (item.getEndDate() == null || item.getEndDate().trim().isEmpty());
 
@@ -270,10 +339,17 @@ public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchServ
                             throw new BizException("派工记录与工单不匹配: " + item.getId());
                         }
 
+                        existDispatch.setCode(item.getCode());
+                        existDispatch.setName(item.getName());
                         existDispatch.setRouteId(routeId);
                         existDispatch.setBomId(item.getBomId());
                         existDispatch.setStageId(item.getStageId());
                         existDispatch.setOrderNum(item.getOrderNum());
+                        existDispatch.setProcessNum(item.getProcessNum());
+                        existDispatch.setProcessTimeHour(item.getProcessTimeHour());
+                        existDispatch.setReadyTimeHour(item.getReadyTimeHour());
+                        existDispatch.setMoveNum(item.getMoveNum());
+                        existDispatch.setMoveTimeHour(item.getMoveTimeHour());
                         existDispatch.setIsInspection(item.getIsInspection() != null ? item.getIsInspection() : false);
                         existDispatch.setBeginDate(item.getBeginDate());
                         existDispatch.setEndDate(item.getEndDate());
@@ -293,6 +369,11 @@ public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchServ
                             userItem.setBomId(item.getBomId());
                             userItem.setStageId(item.getStageId());
                             userItem.setOrderNum(item.getOrderNum());
+                            userItem.setProcessNum(item.getProcessNum());
+                            userItem.setProcessTimeHour(item.getProcessTimeHour());
+                            userItem.setReadyTimeHour(item.getReadyTimeHour());
+                            userItem.setMoveNum(item.getMoveNum());
+                            userItem.setMoveTimeHour(item.getMoveTimeHour());
                             userItem.setBeginDate(item.getBeginDate());
                             userItem.setEndDate(item.getEndDate());
                             userItem.setStatus(existDispatch.getStatus());
@@ -310,11 +391,17 @@ public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchServ
                         dispatch.setOrderId(vo.getOrderId());
                         dispatch.setTenantId(tenantId);
                         String dispatchCode = flowNoService.generateProductOrderDispatchCode(dispatch, connection);
-                        dispatch.setCode(dispatchCode);
+                        dispatch.setCode((item.getCode() != null && !item.getCode().trim().isEmpty()) ? item.getCode() : dispatchCode);
+                        dispatch.setName(item.getName());
                         dispatch.setRouteId(routeId);
                         dispatch.setBomId(item.getBomId());
                         dispatch.setStageId(item.getStageId());
                         dispatch.setOrderNum(item.getOrderNum());
+                        dispatch.setProcessNum(item.getProcessNum());
+                        dispatch.setProcessTimeHour(item.getProcessTimeHour());
+                        dispatch.setReadyTimeHour(item.getReadyTimeHour());
+                        dispatch.setMoveNum(item.getMoveNum());
+                        dispatch.setMoveTimeHour(item.getMoveTimeHour());
                         dispatch.setIsInspection(item.getIsInspection() != null ? item.getIsInspection() : false);
                         dispatch.setBeginDate(item.getBeginDate());
                         dispatch.setEndDate(item.getEndDate());
@@ -328,6 +415,9 @@ public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchServ
 
                         jdbcClient.jdbcInsert(dispatch, connection);
 
+                        // 生成领料单(如果是首道工序)
+                        materialRequisitionService.generateRequisition(dispatch, connection);
+
                         for (Long userIdItem : item.getUserIds()) {
                             DispatchUserItem userItem = new DispatchUserItem();
                             userItem.setDispatchId(dispatch.getId());
@@ -337,6 +427,11 @@ public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchServ
                             userItem.setBomId(item.getBomId());
                             userItem.setStageId(item.getStageId());
                             userItem.setOrderNum(item.getOrderNum());
+                            userItem.setProcessNum(item.getProcessNum());
+                            userItem.setProcessTimeHour(item.getProcessTimeHour());
+                            userItem.setReadyTimeHour(item.getReadyTimeHour());
+                            userItem.setMoveNum(item.getMoveNum());
+                            userItem.setMoveTimeHour(item.getMoveTimeHour());
                             userItem.setBeginDate(item.getBeginDate());
                             userItem.setEndDate(item.getEndDate());
                             userItem.setStatus(MESEnum.PRODUCT_ORDER_DISPATCH_OF_STATUS_PENDING.getValue());
@@ -437,4 +532,189 @@ public class ProductOrderDispatchServiceImpl implements ProductOrderDispatchServ
             connection.close();
         }
     }
+    
+    /**
+     * 批量计算派工单的领料状态(基于领料单出库状态)
+     * 
+     * 业务逻辑说明:
+     * 1. 只有首道工序需要关心领料状态(原材料是否领取)
+     * 2. 非首道工序的"料"来自上一道工序的产出,不需要关心原材料领料
+     * 3. 非首道工序应该关心上一道工序的报工状态(另外的字段)
+     * 
+     * @param voList 派工单VO列表
+     * @param connection 数据库连接
+     */
+    private void calculateRequisitionStatus(List<ProductOrderDispatchVO> voList, Connection connection) throws Exception {
+        // 缓存:工单ID+BOM ID → 领料单列表
+        Map<String, List<MaterialRequisition>> orderBomRequisitionMap = new HashMap<>();
+        
+        // 收集所有首道工序的查询条件
+        for (ProductOrderDispatchVO vo : voList) {
+            if (vo.getId() == null) {
+                continue;
+            }
+            
+            // 判断是否为首道工序
+            boolean isFirstStage = isFirstStage(vo, connection);
+            
+            if (isFirstStage) {
+                // 只有首道工序才查询领料单
+                if (vo.getOrderId() != null && vo.getBomId() != null) {
+                    String key = vo.getOrderId() + "_" + vo.getBomId();
+                    if (!orderBomRequisitionMap.containsKey(key)) {
+                        MaterialRequisition requisitionQuery = new MaterialRequisition();
+                        requisitionQuery.setOrderId(vo.getOrderId());
+                        requisitionQuery.setBomId(vo.getBomId());
+                        List<MaterialRequisition> requisitions = jdbcClient.getJdbcList(requisitionQuery, connection);
+                        orderBomRequisitionMap.put(key, requisitions != null ? requisitions : new ArrayList<>());
+                    }
+                }
+            }
+        }
+        
+        // 为每个派工单计算领料状态
+        for (ProductOrderDispatchVO vo : voList) {
+            if (vo.getId() == null) {
+                vo.setRequisitionStatus(null);  // 无效派工单,不显示状态
+                continue;
+            }
+            
+            // 判断是否为首道工序
+            boolean isFirstStage = isFirstStage(vo, connection);
+            
+            if (isFirstStage) {
+                // 首道工序:显示领料状态
+                String key = vo.getOrderId() + "_" + vo.getBomId();
+                List<MaterialRequisition> requisitions = orderBomRequisitionMap.get(key);
+                
+                // 计算领料状态
+                String requisitionStatus = calculateRequisitionStatusByRequisitions(requisitions);
+                vo.setRequisitionStatus(requisitionStatus);
+                
+                // 设置领料单信息
+                if (requisitions != null && !requisitions.isEmpty()) {
+                    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());
+                        simpleRequisitions.add(simpleReq);
+                    }
+                    vo.setMaterialRequisitions(simpleRequisitions);
+                }
+            } else {
+                // 非首道工序:不显示领料状态(前端可以判断 null 不显示该字段)
+                vo.setRequisitionStatus(null);
+                vo.setMaterialRequisitions(null);
+            }
+        }
+    }
+    
+    /**
+     * 判断派工单是否为首道工序
+     * 
+     * @param vo 派工单VO
+     * @param connection 数据库连接
+     * @return true-首道工序,false-非首道工序
+     */
+    private boolean isFirstStage(ProductOrderDispatchVO vo, Connection connection) throws Exception {
+        if (vo.getRouteId() == null || vo.getStageId() == null) {
+            return false;
+        }
+        
+        // 查询该工艺路线的所有工序
+        ProcessRouteDetail allRouteQuery = new ProcessRouteDetail();
+        allRouteQuery.setRouteId(vo.getRouteId());
+        List<ProcessRouteDetail> allRouteDetails = jdbcClient.getJdbcList(allRouteQuery, connection);
+        
+        if (allRouteDetails == null || allRouteDetails.isEmpty()) {
+            return false;
+        }
+        
+        // 找出最小的 order_num
+        Integer minOrderNum = allRouteDetails.stream()
+            .map(ProcessRouteDetail::getOrderNum)
+            .filter(num -> num != null)
+            .min(Integer::compareTo)
+            .orElse(null);
+        
+        if (minOrderNum == null) {
+            return false;
+        }
+        
+        // 查询当前派工的工序信息
+        ProcessRouteDetail currentRouteQuery = new ProcessRouteDetail();
+        currentRouteQuery.setRouteId(vo.getRouteId());
+        currentRouteQuery.setStageId(vo.getStageId());
+        List<ProcessRouteDetail> currentRouteDetails = jdbcClient.getJdbcList(currentRouteQuery, connection);
+        
+        if (currentRouteDetails == null || currentRouteDetails.isEmpty()) {
+            return false;
+        }
+        
+        ProcessRouteDetail currentRouteDetail = currentRouteDetails.get(0);
+        return currentRouteDetail.getOrderNum() != null 
+            && currentRouteDetail.getOrderNum().equals(minOrderNum);
+    }
+    
+    /**
+     * 根据领料单列表计算领料状态
+     * 规则:
+     * - applied/pending → 未领料
+     * - partial → 部分领料
+     * - completed → 已领料
+     * - 混合状态 → 部分领料
+     * 
+     * @param requisitions 领料单列表
+     * @return 领料状态
+     */
+    private String calculateRequisitionStatusByRequisitions(List<MaterialRequisition> requisitions) {
+        // 没有领料单,返回"未领料"
+        if (requisitions == null || requisitions.isEmpty()) {
+            return MESEnum.DISPATCH_REQUISITION_STATUS_PENDING.getValue();
+        }
+        
+        // 统计出库状态
+        boolean hasCompleted = false;  // 出库完毕
+        boolean hasPartial = false;    // 部分出库
+        boolean hasAppliedOrPending = false;  // 申请完毕或未出库
+        
+        for (MaterialRequisition req : requisitions) {
+            String status = req.getOutboundStatus();
+            if (MESEnum.MATERIAL_REQUISITION_OUTBOUND_STATUS_COMPLETED.getValue().equals(status)) {
+                hasCompleted = true;
+            } else if (MESEnum.MATERIAL_REQUISITION_OUTBOUND_STATUS_PARTIAL.getValue().equals(status)) {
+                hasPartial = true;
+            } else if (MESEnum.MATERIAL_REQUISITION_OUTBOUND_STATUS_APPLIED.getValue().equals(status) 
+                    || MESEnum.MATERIAL_REQUISITION_OUTBOUND_STATUS_PENDING.getValue().equals(status)) {
+                hasAppliedOrPending = true;
+            }
+        }
+        
+        // 判断领料状态
+        if (hasCompleted && !hasPartial && !hasAppliedOrPending) {
+            // 全部出库完毕 → 已领料
+            return MESEnum.DISPATCH_REQUISITION_STATUS_COMPLETE.getValue();
+        } else if (hasAppliedOrPending && !hasPartial && !hasCompleted) {
+            // 全部是申请完毕/未出库 → 未领料
+            return MESEnum.DISPATCH_REQUISITION_STATUS_PENDING.getValue();
+        } else {
+            // 其他情况(有部分出库,或混合状态)→ 部分领料
+            return MESEnum.DISPATCH_REQUISITION_STATUS_PARTIALLY.getValue();
+        }
+    }
 }

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

@@ -364,7 +364,7 @@ public class ProductOrderServiceImpl implements ProductOrderService {
             return false;
         }
 
-        // 3. 每个 BOM 必须关联工艺路线,且工艺路线必须有工序
+        // 3. 每个 BOM 必须关联工艺路线,且工艺路线必须有可报工工序(isReport=true)
         for (ProductOrderBom bom : bomList) {
             // 3.1 检查是否有工艺路线
             if (bom.getRouteId() == null || bom.getRouteId().trim().isEmpty()) {
@@ -381,6 +381,18 @@ public class ProductOrderServiceImpl implements ProductOrderService {
             if (detailList == null || detailList.isEmpty()) {
                 return false;
             }
+
+            // 3.3 至少要有一个可派工工序(isReport=true)
+            boolean hasReportStage = false;
+            for (ProcessRouteDetail detail : detailList) {
+                if (Boolean.TRUE.equals(detail.getIsReport()) && detail.getStageId() != null) {
+                    hasReportStage = true;
+                    break;
+                }
+            }
+            if (!hasReportStage) {
+                return false;
+            }
         }
 
         // 4. 检查是否还有未派完工的工序
@@ -410,7 +422,8 @@ public class ProductOrderServiceImpl implements ProductOrderService {
             ProcessRoute route = bom.getProcessRoute();
             if (route != null && route.getDetailList() != null) {
                 for (ProcessRouteDetail detail : route.getDetailList()) {
-                    if (detail.getStageId() != null) {
+                    // 仅可报工工序(isReport=true)参与派工判断
+                    if (Boolean.TRUE.equals(detail.getIsReport()) && detail.getStageId() != null) {
                         // 使用 bomId(product_bom 的 ID)而不是 product_order_bom 的 id
                         String key = bom.getBomId() + "_" + detail.getStageId();
                         double dispatchedQty = dispatchedQuantityMap.getOrDefault(key, 0.0);