4 天以前 5140e16bd1cb2bdf974d47a4342f9e27ef804655
feat(erp): 增加销售订单MPS集成与发货通知单控制功能

- 在销售订单控制器中新增MesProMpsApi依赖注入
- 实现销售订单审核通过时自动创建MPS的功能
- 增加检查是否可以生成发货通知单的逻辑判断
- 添加销售订单响应VO中的canCreateSalesNotice和needProduction字段
- 删除原有的销售订单状态监听器并将其逻辑整合到服务层
- 优化供应商服务中的空集合处理逻辑
- 在MES模块中实现checkCanCreateSalesNotice接口方法
- 更新质量检验服务中物料校验的API调用方式
- 修改仓库事务服务中出库时的库存检查逻辑,支持负库存配置
已修改8个文件
已删除1个文件
370 ■■■■ 文件已修改
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpSupplierServiceImpl.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/listener/ErpSaleOrderStatusListener.java 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/mps/MesProMpsApi.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/mps/MesProMpsApiImpl.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java
@@ -18,6 +18,7 @@
import cn.iocoder.yudao.module.erp.service.sale.ErpSaleOrderService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi;
import cn.iocoder.yudao.module.mes.api.salesnotice.MesWmSalesNoticeApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
@@ -58,6 +59,8 @@
    private AdminUserApi adminUserApi;
    @Resource
    private MesWmSalesNoticeApi salesNoticeApi;
    @Resource
    private MesProMpsApi mpsApi;
    @PostMapping("/create")
    @Operation(summary = "创建销售订单")
@@ -171,6 +174,40 @@
        // 1.5 å‘货通知单信息
        Map<Long, Boolean> salesNoticeMap = salesNoticeApi.checkSalesNoticeBySaleOrderIds(
                convertSet(pageResult.getList(), ErpSaleOrderDO::getId));
        // 1.6 æ£€æŸ¥æ˜¯å¦å¯ä»¥ç”Ÿæˆå‘货通知单(需要生产的明细对应的 MPS æ˜¯å¦å·²å®Œæˆï¼‰
        // ç­›é€‰å‡ºéœ€è¦ç”Ÿäº§çš„æ˜Žç»†ID
        List<Long> needProductionItemIds = saleOrderItemList.stream()
                .filter(item -> item.getNeedProduction() != null && item.getNeedProduction() == 1)
                .map(ErpSaleOrderItemDO::getId)
                .toList();
        Map<Long, Boolean> canCreateSalesNoticeMap = new java.util.HashMap<>();
        if (!needProductionItemIds.isEmpty()) {
            // æŒ‰è®¢å•分组明细ID
            Map<Long, List<Long>> orderItemIdsMap = new java.util.HashMap<>();
            for (ErpSaleOrderItemDO item : saleOrderItemList) {
                orderItemIdsMap.computeIfAbsent(item.getOrderId(), k -> new java.util.ArrayList<>())
                        .add(item.getId());
            }
            // æ£€æŸ¥æ¯ä¸ªè®¢å•
            for (Map.Entry<Long, List<Long>> entry : orderItemIdsMap.entrySet()) {
                List<Long> itemIds = entry.getValue().stream()
                        .filter(id -> {
                            ErpSaleOrderItemDO item = saleOrderItemList.stream()
                                    .filter(i -> i.getId().equals(id))
                                    .findFirst().orElse(null);
                            return item != null && item.getNeedProduction() != null && item.getNeedProduction() == 1;
                        }
                ).toList();
                if (itemIds.isEmpty()) {
                    canCreateSalesNoticeMap.put(entry.getKey(), true);
                } else {
                    canCreateSalesNoticeMap.put(entry.getKey(), mpsApi.checkCanCreateSalesNotice(itemIds));
                }
            }
        } else {
            // æ²¡æœ‰éœ€è¦ç”Ÿäº§çš„æ˜Žç»†ï¼Œéƒ½å¯ä»¥ç”Ÿæˆå‘货通知单
            pageResult.getList().forEach(order -> canCreateSalesNoticeMap.put(order.getId(), true));
        }
        // 2. å¼€å§‹æ‹¼æŽ¥
        return BeanUtils.toBean(pageResult, ErpSaleOrderRespVO.class, saleOrder -> {
            saleOrder.setItems(BeanUtils.toBean(saleOrderItemMap.get(saleOrder.getId()), ErpSaleOrderRespVO.Item.class,
@@ -181,6 +218,12 @@
            MapUtils.findAndThen(userMap, Long.parseLong(saleOrder.getCreator()), user -> saleOrder.setCreatorName(user.getNickname()));
            // è®¾ç½®æ˜¯å¦å·²ç”Ÿæˆå‘货通知单
            saleOrder.setHasSalesNotice(salesNoticeMap.getOrDefault(saleOrder.getId(), false));
            // è®¾ç½®æ˜¯å¦å¯ä»¥ç”Ÿæˆå‘货通知单
            saleOrder.setCanCreateSalesNotice(canCreateSalesNoticeMap.getOrDefault(saleOrder.getId(), true));
            // è®¾ç½®æ˜¯å¦æœ‰éœ€è¦ç”Ÿäº§çš„æ˜Žç»†
            boolean hasNeedProduction = saleOrder.getItems().stream()
                    .anyMatch(item -> item.getNeedProduction() != null && item.getNeedProduction() == 1);
            saleOrder.setNeedProduction(hasNeedProduction ? 1 : 0);
        });
    }
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java
@@ -109,6 +109,12 @@
    @Schema(description = "是否已生成发货通知单", example = "false")
    private Boolean hasSalesNotice;
    @Schema(description = "是否可以生成发货通知单(有需要生产且未完成的明细时为 false)", example = "true")
    private Boolean canCreateSalesNotice;
    @Schema(description = "是否有需要生产的明细(0=否 1=是)", example = "1")
    private Integer needProduction;
    @Data
    public static class Item {
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpSupplierServiceImpl.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.erp.service.purchase;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@@ -78,6 +79,9 @@
    @Override
    public List<ErpSupplierDO> getSupplierList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return CollUtil.newArrayList();
        }
        return supplierMapper.selectByIds(ids);
    }
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -17,13 +17,20 @@
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
import cn.iocoder.yudao.module.erp.api.sale.enums.ErpSaleOrderOutStatusEnum;
import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi;
import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsCreateReqDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -40,6 +47,7 @@
 */
@Service
@Validated
@Slf4j
public class ErpSaleOrderServiceImpl implements ErpSaleOrderService {
    @Resource
@@ -59,6 +67,11 @@
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    private MesProMpsApi mpsApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -157,6 +170,85 @@
        if (updateCount == 0) {
            throw exception(approve ? SALE_ORDER_APPROVE_FAIL : SALE_ORDER_PROCESS_FAIL);
        }
        // 3. å®¡æ ¸é€šè¿‡æ—¶ï¼Œä¸ºéœ€è¦ç”Ÿäº§çš„æ˜Žç»†åˆ›å»º MPS
        if (approve) {
            createMpsForSaleOrder(id, saleOrder);
        }
    }
    /**
     * ä¸ºé”€å”®è®¢å•中需要生产的明细创建 MPS
     *
     * @param orderId   é”€å”®è®¢å•编号
     * @param saleOrder é”€å”®è®¢å•
     */
    private void createMpsForSaleOrder(Long orderId, ErpSaleOrderDO saleOrder) {
        List<ErpSaleOrderItemDO> items = saleOrderItemMapper.selectListByOrderId(orderId);
        if (CollUtil.isEmpty(items)) {
            return;
        }
        int mpsCount = 0;
        for (ErpSaleOrderItemDO item : items) {
            if (item.getNeedProduction() == null || item.getNeedProduction() != 1) {
                continue;
            }
            try {
                Long mpsId = createMpsForSaleOrderItem(saleOrder, item);
                if (mpsId != null) {
                    mpsCount++;
                    // æ›´æ–°é”€å”®è®¢å•明细的 MPS å…³è”
                    saleOrderItemMapper.updateById(new ErpSaleOrderItemDO()
                            .setId(item.getId())
                            .setMpsId(mpsId));
                }
            } catch (Exception e) {
                log.error("[createMpsForSaleOrder] åˆ›å»º MPS å¤±è´¥ï¼ŒsaleOrderItemId={}", item.getId(), e);
            }
        }
        log.info("[createMpsForSaleOrder] é”€å”®è®¢å•({}) å®¡æ ¸å®Œæˆï¼Œåˆ›å»º MPS {} æ¡", orderId, mpsCount);
    }
    /**
     * ä¸ºé”€å”®è®¢å•明细创建 MPS
     */
    private Long createMpsForSaleOrderItem(ErpSaleOrderDO saleOrder, ErpSaleOrderItemDO item) {
        // èŽ·å– MDM ç‰©æ–™ä¿¡æ¯ï¼ˆç”¨äºŽèŽ·å–äº§å“åç§°ï¼‰
        String productName = null;
        try {
            MdmItemRespDTO mdmItem = mdmItemApi.getItem(item.getProductId()).getCheckedData();
            if (mdmItem != null) {
                productName = mdmItem.getName();
            }
        } catch (Exception e) {
            log.warn("[createMpsForSaleOrderItem] èŽ·å– MDM ç‰©æ–™ä¿¡æ¯å¤±è´¥ï¼ŒproductId={}", item.getProductId(), e);
        }
        MesProMpsCreateReqDTO createReqDTO = new MesProMpsCreateReqDTO();
        createReqDTO.setSaleOrderId(saleOrder.getId());
        createReqDTO.setSaleOrderNo(saleOrder.getNo());
        createReqDTO.setSaleOrderItemId(item.getId());
        createReqDTO.setMdmItemId(item.getProductId());
        createReqDTO.setProductName(productName);
        createReqDTO.setQuantity(item.getCount());
        createReqDTO.setClientId(saleOrder.getCustomerId());
        createReqDTO.setRequestDate(formatRequestDate(saleOrder.getOrderTime()));
        createReqDTO.setRemark("自动生成,来自销售订单:" + saleOrder.getNo());
        Long mpsId = mpsApi.createMps(createReqDTO);
        log.info("[createMpsForSaleOrderItem] MPS åˆ›å»ºæˆåŠŸï¼ŒmpsId={}, saleOrderItemId={}", mpsId, item.getId());
        return mpsId;
    }
    /**
     * æ ¼å¼åŒ–需求日期
     */
    private String formatRequestDate(LocalDateTime dateTime) {
        if (dateTime == null) {
            return null;
        }
        return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }
    @Override
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/listener/ErpSaleOrderStatusListener.java
ÎļþÒÑɾ³ý
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/mps/MesProMpsApi.java
@@ -46,4 +46,14 @@
     */
    List<MesProMpsRespDTO> getMpsListBySaleOrderItemIds(List<Long> saleOrderItemIds);
    /**
     * æ£€æŸ¥é”€å”®è®¢å•是否可以生成发货通知单
     *
     * è§„则:如果有需要生产的明细,对应的 MPS å¿…须已完成(status=2)或已取消(status=3)
     *
     * @param saleOrderItemIds é”€å”®è®¢å•明细ID列表
     * @return true=可以生成发货通知单,false=不可以
     */
    Boolean checkCanCreateSalesNotice(List<Long> saleOrderItemIds);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/mps/MesProMpsApiImpl.java
@@ -1,9 +1,11 @@
package cn.iocoder.yudao.module.mes.api.mps;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsCreateReqDTO;
import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsDO;
import cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum;
import cn.iocoder.yudao.module.mes.service.pro.mps.MesProMpsService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -11,6 +13,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
 * MPS API å®žçŽ°ç±»
@@ -60,4 +63,36 @@
        return BeanUtils.toBean(mpsList, MesProMpsRespDTO.class);
    }
    @Override
    public Boolean checkCanCreateSalesNotice(List<Long> saleOrderItemIds) {
        if (CollUtil.isEmpty(saleOrderItemIds)) {
            return true;
        }
        List<MesProMpsDO> mpsList = mpsService.getMpsListBySaleOrderItemIds(saleOrderItemIds);
        // æž„建 saleOrderItemId -> MPS çš„æ˜ å°„
        Map<Long, MesProMpsDO> mpsMap = new java.util.HashMap<>();
        for (MesProMpsDO mps : mpsList) {
            if (mps.getSaleOrderItemId() != null) {
                mpsMap.put(mps.getSaleOrderItemId(), mps);
            }
        }
        // æ£€æŸ¥æ¯ä¸ªé”€å”®è®¢å•明细:
        // - å¦‚æžœ MPS ä¸å­˜åœ¨ï¼šä¸èƒ½ç”Ÿæˆå‘货通知单
        // - å¦‚æžœ MPS å­˜åœ¨ä¸”状态为草稿/已下发:不能生成发货通知单
        // - å¦‚æžœ MPS å­˜åœ¨ä¸”状态为已完成/已取消:可以生成发货通知单
        for (Long itemId : saleOrderItemIds) {
            MesProMpsDO mps = mpsMap.get(itemId);
            if (mps == null) {
                // MPS ä¸å­˜åœ¨ï¼Œä¸èƒ½ç”Ÿæˆå‘货通知单
                return false;
            }
            if (mps.getStatus().equals(MesProMpsStatusEnum.DRAFT.getStatus())
                    || mps.getStatus().equals(MesProMpsStatusEnum.ISSUED.getStatus())) {
                // MPS æœªå®Œæˆï¼Œä¸èƒ½ç”Ÿæˆå‘货通知单
                return false;
            }
        }
        return true;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java
@@ -13,7 +13,6 @@
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
@@ -57,9 +56,6 @@
    @Resource
    @Lazy
    private MesQcDefectRecordService defectRecordService;
    @Resource
    @Lazy
    private MesMdItemService itemService;
    @Resource
    @Lazy
    private MesWmProductSalesLineService productSalesLineService;
@@ -121,8 +117,8 @@
        // æ ¡éªŒç¼–号唯一
        validateOqcCodeUnique(reqVO.getId(), reqVO.getCode());
        // æ ¡éªŒç‰©æ–™ã€æ£€æµ‹äººå‘˜å­˜åœ¨
        // æ³¨ï¼šå®¢æˆ· ID æ¥è‡ª CRM,不需要在 MES ä¸­æ ¡éªŒ
        itemService.validateItemExists(reqVO.getItemId());
        // æ³¨ï¼šç‰©æ–™ ID æ¥è‡ª MDM,客户 ID æ¥è‡ª CRM
        mdmItemApi.validateItemExists(reqVO.getItemId());
        adminUserApi.validateUser(reqVO.getInspectorUserId());
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java
@@ -100,12 +100,20 @@
                    reqDTO.getItemId(), reqDTO.getWarehouseId(), reqDTO.getLocationId(), reqDTO.getAreaId(),
                    reqDTO.getBatchId(), reqDTO.getBatchCode(), reqDTO.getVendorId(), reqDTO.getReceiptTime());
        } else {
            // å‡ºåº“事务:库存记录必须已存在,不允许自动创建
            // å‡ºåº“事务:库存记录必须已存在,不允许自动创建(除非 checkFlag=false å…è®¸è´Ÿåº“存)
            materialStock = materialStockService.getMaterialStockByCompositeKey(
                    reqDTO.getItemId(), reqDTO.getWarehouseId(), reqDTO.getLocationId(), reqDTO.getAreaId(),
                    reqDTO.getBatchId(), reqDTO.getVendorId());
            if (materialStock == null) {
                throw exception(WM_MATERIAL_STOCK_NOT_EXISTS);
                // checkFlag=false æ—¶å…è®¸è´Ÿåº“存,自动创建库存记录(数量将从 0 æ‰£å‡ä¸ºè´Ÿæ•°ï¼‰
                boolean allowNegative = !ObjUtil.defaultIfNull(reqDTO.getCheckFlag(), true);
                if (allowNegative) {
                    materialStock = materialStockService.getOrCreateMaterialStock(
                            reqDTO.getItemId(), reqDTO.getWarehouseId(), reqDTO.getLocationId(), reqDTO.getAreaId(),
                            reqDTO.getBatchId(), reqDTO.getBatchCode(), reqDTO.getVendorId(), reqDTO.getReceiptTime());
                } else {
                    throw exception(WM_MATERIAL_STOCK_NOT_EXISTS);
                }
            }
        }
        if (materialStock == null || materialStock.getId() == null) {