From f785e619ff7cbe99aa80fc7679a244224ab5024f Mon Sep 17 00:00:00 2001
From: tuqin <tuqin@tuqin.cn>
Date: 星期二, 03 十一月 2026 09:19:47 +0800
Subject: [PATCH] feat(erp): 生产车间可从销售订单直接生成生产计划(结合库存判断)

---
 yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java |  196 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 196 insertions(+), 0 deletions(-)

diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
index 18985aa..cdb54f5 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -52,9 +52,13 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@@ -98,6 +102,9 @@
     private CrmContractApi contractApi;
     @Resource
     private ErpAccountService accountService;
+
+    @Resource
+    private cn.iocoder.yudao.module.mes.api.wms.MesWmStockApi mesWmStockApi;
 
     @Resource
     private AdminUserApi adminUserApi;
@@ -561,6 +568,195 @@
         return mpsId;
     }
 
+    // ==================== 浠庨攢鍞鍗曠敓鎴愮敓浜ц鍒掞紙鐢熶骇杞﹂棿瑙嗚锛� ====================
+
+    @Override
+    public List<cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderPendingProductionRespVO>
+            getPendingProductionList() {
+        // 1. 鍙栧叏閮ㄥ凡瀹℃牳鐨勯攢鍞鍗�
+        List<ErpSaleOrderDO> orders = saleOrderMapper.selectList(
+                new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX<ErpSaleOrderDO>()
+                        .eq(ErpSaleOrderDO::getStatus, ErpAuditStatus.APPROVE.getStatus())
+                        .orderByDesc(ErpSaleOrderDO::getId));
+        if (CollUtil.isEmpty(orders)) {
+            return Collections.emptyList();
+        }
+        Map<Long, ErpSaleOrderDO> orderMap = orders.stream()
+                .collect(Collectors.toMap(ErpSaleOrderDO::getId, o -> o, (a, b) -> a));
+
+        // 2. 鍙栬鍗曟槑缁�
+        List<ErpSaleOrderItemDO> items = saleOrderItemMapper.selectListByOrderIds(orderMap.keySet());
+        if (CollUtil.isEmpty(items)) {
+            return Collections.emptyList();
+        }
+
+        // 3. 宸叉湁鏈夋晥 MPS 鐨勬槑缁嗚鎺掗櫎锛岄伩鍏嶉噸澶嶅畨鎺掔敓浜�
+        Set<Long> plannedItemIds = new HashSet<>();
+        List<Long> allItemIds = items.stream().map(ErpSaleOrderItemDO::getId).collect(Collectors.toList());
+        try {
+            // 娉ㄦ剰锛氳 API 鐩存帴杩斿洖 List锛屼笉鏄� CommonResult
+            List<cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsRespDTO> existMps =
+                    mpsApi.getMpsListBySaleOrderItemIds(allItemIds);
+            if (CollUtil.isNotEmpty(existMps)) {
+                existMps.stream()
+                        .filter(m -> ObjectUtil.notEqual(m.getStatus(), MPS_STATUS_CANCELED))
+                        .map(cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsRespDTO::getSaleOrderItemId)
+                        .filter(Objects::nonNull)
+                        .forEach(plannedItemIds::add);
+            }
+        } catch (Exception e) {
+            log.warn("[getPendingProductionList] 鏌ヨ宸叉湁 MPS 澶辫触锛屾寜鍏ㄩ儴鍙畨鎺掑鐞�", e);
+        }
+
+        // 4. 缁勮锛氬甫鍑哄彲鐢ㄥ簱瀛樹笌寤鸿鐢熶骇鏁伴噺
+        List<cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderPendingProductionRespVO> result
+                = new ArrayList<>();
+        for (ErpSaleOrderItemDO item : items) {
+            if (plannedItemIds.contains(item.getId())) {
+                continue;
+            }
+            ErpSaleOrderDO order = orderMap.get(item.getOrderId());
+            if (order == null) {
+                continue;
+            }
+            cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderPendingProductionRespVO vo
+                    = new cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderPendingProductionRespVO();
+            vo.setSaleOrderId(order.getId());
+            vo.setSaleOrderNo(order.getNo());
+            vo.setSaleOrderItemId(item.getId());
+            vo.setCustomerId(order.getCustomerId());
+            vo.setCustomerName(getCustomerName(order.getCustomerId()));
+            vo.setProductId(item.getProductId());
+            // 鏄庣粏琛ㄥ彧瀛� productId锛屼骇鍝佺紪鐮�/鍚嶇О/瑙勬牸浠� MDM 鍙�
+            try {
+                MdmItemRespDTO mdmItem = mdmItemApi.getItem(item.getProductId()).getCheckedData();
+                if (mdmItem != null) {
+                    vo.setProductCode(mdmItem.getCode());
+                    vo.setProductName(mdmItem.getName());
+                    vo.setProductSpecification(mdmItem.getSpecification());
+                }
+            } catch (Exception e) {
+                log.warn("[getPendingProductionList] 鑾峰彇鐗╂枡淇℃伅澶辫触锛宲roductId={}", item.getProductId(), e);
+            }
+            vo.setOrderCount(item.getCount());
+            vo.setOrderTime(order.getOrderTime());
+
+            // 褰撳墠鍙敤搴撳瓨
+            BigDecimal stock = BigDecimal.ZERO;
+            try {
+                BigDecimal qty = mesWmStockApi.getStockQuantity(item.getProductId());
+                if (qty != null) {
+                    stock = qty;
+                }
+            } catch (Exception e) {
+                log.warn("[getPendingProductionList] 鏌ヨ搴撳瓨澶辫触锛宲roductId={}", item.getProductId(), e);
+            }
+            vo.setStockQuantity(stock);
+
+            // 寤鸿鐢熶骇鏁伴噺 = 璁㈣喘鏁伴噺 - 鍙敤搴撳瓨锛屼笉瓒� 0 鍒欏彇 0
+            BigDecimal count = item.getCount() == null ? BigDecimal.ZERO : item.getCount();
+            BigDecimal suggest = count.subtract(stock);
+            vo.setSuggestQuantity(suggest.compareTo(BigDecimal.ZERO) > 0 ? suggest : BigDecimal.ZERO);
+
+            result.add(vo);
+        }
+        // 鎸夊缓璁敓浜ф暟閲忛檷搴忥紝鎬ラ渶鐢熶骇鐨勬帓鍓嶉潰
+        result.sort((a, b) -> b.getSuggestQuantity().compareTo(a.getSuggestQuantity()));
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int generateMpsFromSaleOrder(
+            cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderGenerateMpsReqVO reqVO) {
+        if (reqVO.getItems() == null || reqVO.getItems().isEmpty()) {
+            return 0;
+        }
+        int success = 0;
+        for (cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderGenerateMpsReqVO.Item reqItem
+                : reqVO.getItems()) {
+            ErpSaleOrderItemDO item = saleOrderItemMapper.selectById(reqItem.getSaleOrderItemId());
+            if (item == null) {
+                log.warn("[generateMpsFromSaleOrder] 閿�鍞鍗曟槑缁嗕笉瀛樺湪锛宨d={}", reqItem.getSaleOrderItemId());
+                continue;
+            }
+            ErpSaleOrderDO order = saleOrderMapper.selectById(item.getOrderId());
+            if (order == null || !ErpAuditStatus.APPROVE.getStatus().equals(order.getStatus())) {
+                log.warn("[generateMpsFromSaleOrder] 閿�鍞鍗曟湭瀹℃牳锛岃烦杩囷紝itemId={}", item.getId());
+                continue;
+            }
+            // 骞傜瓑淇濇姢锛氬凡鏈夋湁鏁� MPS 鍒欒烦杩囷紝閬垮厤閲嶅瀹夋帓
+            try {
+                List<cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsRespDTO> existMps =
+                        mpsApi.getMpsListBySaleOrderItemIds(Collections.singletonList(item.getId()));
+                if (CollUtil.isNotEmpty(existMps)
+                        && existMps.stream().anyMatch(m -> ObjectUtil.notEqual(m.getStatus(), MPS_STATUS_CANCELED))) {
+                    log.info("[generateMpsFromSaleOrder] 鏄庣粏({}) 宸插瓨鍦ㄦ湁鏁� MPS锛岃烦杩�", item.getId());
+                    continue;
+                }
+            } catch (Exception e) {
+                log.warn("[generateMpsFromSaleOrder] 骞傜瓑鏍¢獙澶辫触锛岀户缁敓鎴愶紝itemId={}", item.getId(), e);
+            }
+            try {
+                createMpsForSaleOrderItemWithQuantity(order, item, reqItem.getQuantity());
+                success++;
+            } catch (Exception e) {
+                log.error("[generateMpsFromSaleOrder] 鐢熸垚 MPS 澶辫触锛宨temId={}", item.getId(), e);
+            }
+        }
+        log.info("[generateMpsFromSaleOrder] 鍏辩敓鎴� MPS {} 鏉�", success);
+        return success;
+    }
+
+    /**
+     * 鎸夋寚瀹氭暟閲忎负閿�鍞鍗曟槑缁嗗垱寤� MPS
+     *
+     * 涓� createMpsForSaleOrderItem 鐨勫樊鍒細鏁伴噺鐢辫溅闂磋嚜琛屾寚瀹氾紙閫氬父鏄� 璁㈣喘鏁伴噺 - 鍙敤搴撳瓨锛夛紝
+     * 鑰屼笉鏄洿鎺ョ敤璁㈠崟鏁伴噺
+     */
+    private Long createMpsForSaleOrderItemWithQuantity(ErpSaleOrderDO saleOrder, ErpSaleOrderItemDO item,
+                                                       BigDecimal quantity) {
+        String productName = null;
+        try {
+            MdmItemRespDTO mdmItem = mdmItemApi.getItem(item.getProductId()).getCheckedData();
+            if (mdmItem != null) {
+                productName = mdmItem.getName();
+            }
+        } catch (Exception e) {
+            log.warn("[createMpsForSaleOrderItemWithQuantity] 鑾峰彇鐗╂枡淇℃伅澶辫触锛宲roductId={}", 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(quantity);
+        createReqDTO.setClientId(saleOrder.getCustomerId());
+        createReqDTO.setRequestDate(formatRequestDate(saleOrder.getOrderTime()));
+        createReqDTO.setRemark("鐢辩敓浜ц溅闂翠粠閿�鍞鍗曠敓鎴愶細" + saleOrder.getNo());
+        return mpsApi.createMps(createReqDTO);
+    }
+
+    /**
+     * 鍙栧鎴峰悕绉帮紝澶辫触鏃惰繑鍥� null 涓嶅奖鍝嶄富娴佺▼
+     */
+    private String getCustomerName(Long customerId) {
+        if (customerId == null) {
+            return null;
+        }
+        try {
+            // 璇� API 鐩存帴杩斿洖 DTO锛屼笉鏄� CommonResult
+            cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO customer =
+                    customerApi.getCustomer(customerId);
+            return customer == null ? null : customer.getName();
+        } catch (Exception e) {
+            log.warn("[getCustomerName] 鑾峰彇瀹㈡埛澶辫触锛宑ustomerId={}", customerId, e);
+            return null;
+        }
+    }
+
     /**
      * 鏍煎紡鍖栭渶姹傛棩鏈�
      */

--
Gitblit v1.9.3