From 7188f28998d47c286e34cbd525986603cfc19ba9 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期五, 31 七月 2026 17:58:25 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_business' into dev_business

---
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderBomController.java |  259 ++++++++++++++++++++++++++++++++++++++++++---------
 1 files changed, 210 insertions(+), 49 deletions(-)

diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderBomController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderBomController.java
index c2d586c..a44b286 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderBomController.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderBomController.java
@@ -12,14 +12,24 @@
 import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.bom.MesProWorkOrderBomSaveReqVO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO;
-import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdProductBomDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper;
+import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi;
+import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderItemRespDTO;
 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService;
-import cn.iocoder.yudao.module.mes.service.md.item.MesMdProductBomService;
 import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
+import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductBomService;
+import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
 import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderBomService;
+import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
+import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -31,6 +41,7 @@
 
 import java.math.BigDecimal;
 import java.util.*;
+import java.util.Comparator;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@@ -44,13 +55,23 @@
     @Resource
     private MesProWorkOrderBomService workOrderBomService;
     @Resource
+    private MesProWorkOrderService workOrderService;
+    @Resource
+    private MesProWorkOrderProcessService workOrderProcessService;
+    @Resource
     private MesMdItemService itemService;
     @Resource
     private MesMdUnitMeasureService unitMeasureService;
     @Resource
     private MesMdItemTypeService itemTypeService;
     @Resource
-    private MesMdProductBomService productBomService;
+    private MesProRouteProductService routeProductService;
+    @Resource
+    private MesProRouteProductBomService routeProductBomService;
+    @Resource
+    private MesWmMaterialStockMapper materialStockMapper;
+    @Resource
+    private ErpPurchaseOrderApi purchaseOrderApi;
 
     @PostMapping("/create")
     @Operation(summary = "鍒涘缓宸ュ崟 BOM")
@@ -102,40 +123,67 @@
     @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')")
     public CommonResult<List<MesProWorkOrderItemRespVO>> getWorkOrderBomItemListByWorkOrderId(
             @RequestParam("workOrderId") Long workOrderId) {
-        // 1. 鑾峰彇宸ュ崟 BOM 琛�
+        // 1. 鑾峰彇宸ュ崟
+        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(workOrderId);
+        if (workOrder == null) {
+            return success(Collections.emptyList());
+        }
+
+        // 2. 鑾峰彇宸ュ崟宸ュ簭鍒楄〃锛堝惈鍚勫伐搴忕殑鎶曟枡鏄庣粏涓庝骇鍑哄搧锛�
+        List<MesProWorkOrderProcessDO> processList = workOrderProcessService
+                .getWorkOrderProcessListByWorkOrderId(workOrderId);
+        if (CollUtil.isEmpty(processList)) {
+
+            return success(Collections.emptyList());
+        }
+
+        // 3. 鑾峰彇宸ュ崟 BOM 琛岋紙鍚勫伐搴忕殑鎶曟枡璁板綍锛屾寜 processId 鍒嗙粍浣跨敤锛�
         List<MesProWorkOrderBomDO> bomList = workOrderBomService.getWorkOrderBomListByWorkOrderId(workOrderId);
         if (CollUtil.isEmpty(bomList)) {
             return success(Collections.emptyList());
         }
 
-        // 2. 閫愬眰灞曞紑锛屽緱鍒板彾瀛愮墿鏂� itemId 鈫� quantity
-        Map<Long, BigDecimal> leafItems = buildWorkOrderItems(bomList);
+        // 4. 閫氳繃浜у搧ID鍙嶆煡宸ヨ壓璺嚎锛堝伐鍗� routeId 鍙兘涓虹┖锛�
+        Long routeId = null;
+        if (workOrder.getProductId() != null) {
+            MesProRouteProductDO routeProduct = routeProductService.getRouteProductByItemId(workOrder.getProductId());
+            if (routeProduct != null) {
+                routeId = routeProduct.getRouteId();
+            }
+        }
+
+        // 5. 鎸夊伐搴忎骇鍑洪摼閫愬眰灞曞紑 BOM锛屽緱鍒板彾瀛愮墿鏂欙紙姣忓崟浣嶆垚鍝佺殑鐢ㄩ噺锛�
+        Map<Long, BigDecimal> leafItems = buildWorkOrderItems(processList, bomList, routeId);
         if (CollUtil.isEmpty(leafItems)) {
             return success(Collections.emptyList());
         }
 
-        // 3. 鎷兼帴 VO 瀛楁
-        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(leafItems.keySet());
-        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
-                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
-        Map<Long, MesMdItemTypeDO> itemTypeMap = itemTypeService.getItemTypeMap(
-                convertSet(itemMap.values(), MesMdItemDO::getItemTypeId));
-        List<MesProWorkOrderItemRespVO> result = new ArrayList<>(leafItems.size());
+        // 6. 涔樹互宸ュ崟鐢熶骇鏁伴噺锛屽緱鍒版�婚渶姹�
+        BigDecimal orderQty = workOrder.getQuantity() != null ? workOrder.getQuantity() : BigDecimal.ONE;
+        Map<Long, BigDecimal> totalRequirements = new LinkedHashMap<>();
         for (Map.Entry<Long, BigDecimal> entry : leafItems.entrySet()) {
-            MesMdItemDO item = itemMap.get(entry.getKey());
-            if (item == null) {
-                continue;
-            }
-            MesProWorkOrderItemRespVO vo = new MesProWorkOrderItemRespVO()
-                    .setItemId(item.getId()).setQuantity(entry.getValue())
-                    .setItemCode(item.getCode()).setItemName(item.getName()).setItemSpecification(item.getSpecification());
-            MapUtils.findAndThen(unitMeasureMap, item.getUnitMeasureId(),
-                    unitMeasure -> vo.setUnitMeasureName(unitMeasure.getName()));
-            MapUtils.findAndThen(itemTypeMap, item.getItemTypeId(),
-                    itemType -> vo.setItemOrProduct(itemType.getItemOrProduct()));
-            result.add(vo);
+            totalRequirements.put(entry.getKey(), entry.getValue().multiply(orderQty));
         }
-        return success(result);
+
+        // 7. 鏌ヨ鏉ユ簮涓烘湰宸ュ崟鐨勯噰璐鍗曟槑缁嗭紝鎸変骇鍝佹眹鎬诲湪閫旈噰璐暟
+        //    sourceType=1 琛ㄧず鏉ユ簮绫诲瀷涓�"鐢熶骇宸ュ崟"
+        Map<Long, BigDecimal> purchaseOrderQtyMap = new HashMap<>();
+        List<ErpPurchaseOrderItemRespDTO> poItems = purchaseOrderApi
+                .getPurchaseOrderItemListBySource(1, workOrderId)
+                .getCheckedData();
+        if (CollUtil.isNotEmpty(poItems)) {
+            for (ErpPurchaseOrderItemRespDTO poItem : poItems) {
+                // 鍦ㄩ�� = 璁㈣喘鏁伴噺 - 宸插叆搴撴暟閲�
+                BigDecimal inCount = poItem.getInCount() != null ? poItem.getInCount() : BigDecimal.ZERO;
+                BigDecimal inTransit = poItem.getCount().subtract(inCount);
+                if (inTransit.compareTo(BigDecimal.ZERO) > 0) {
+                    purchaseOrderQtyMap.merge(poItem.getProductId(), inTransit, BigDecimal::add);
+                }
+            }
+        }
+
+        // 8. 鏌ヨ搴撳瓨锛屾嫾鎺� VO
+        return success(buildItemRespVOList(totalRequirements, purchaseOrderQtyMap));
     }
 
     // ==================== 鎷兼帴 VO ====================
@@ -164,47 +212,160 @@
         });
     }
 
-    // ==================== 鐗╂枡闇�姹傞�愬眰灞曞紑 ====================
+    // ==================== 鐗╂枡闇�姹傚睍寮�锛堝伐搴忎骇鍑洪摼 + 璺敱浜у搧 BOM锛� ====================
 
     /**
-     * 閫愬眰 IN 鏌ヨ灞曞紑宸ュ崟 BOM 鍒板彾瀛愮墿鏂欙紝鍚堝苟鍚岀墿鏂欑殑闇�姹傛暟閲�
+     * 鎸夊伐搴忎骇鍑洪摼閫愬眰灞曞紑鐗╂枡闇�姹傘��
      *
-     * @param bomList 宸ュ崟 BOM 鍒楄〃
-     * @return 鍙跺瓙鐗╂枡 Map锛歩temId 鈫� 鍚堝苟鍚庣殑闇�姹傛暟閲�
+     * <p><b>鏍稿績閫昏緫锛�</b></p>
+     * <ol>
+     *   <li>鎵惧嚭鍏抽敭宸ュ簭锛坘eyFlag=true锛夛紝浠ュ畠鐨勬姇鏂欎负灞曞紑璧风偣</li>
+     *   <li>鏋勫缓浜у嚭鍝佲啋宸ュ簭鎶曟枡鐨勬槧灏勶紙{@code outputItemId 鈫� per-unit bomItems}锛�</li>
+     *   <li>閫愬眰灞曞紑锛氳嫢鐗╂枡鏄煇宸ュ簭鐨勪骇鍑哄搧 鈫� 鐢ㄨ宸ュ簭鐨勬姇鏂欓厤鏂规浛浠o紱
+     *       鍚﹀垯鏌ヨ矾鐢变骇鍝� BOM 鈫� 鐢� BOM 瀛愰」鏇夸唬锛�
+     *       涓よ�呴兘娌℃湁 鈫� 鍙跺瓙鐗╂枡锛堝璐欢锛夛紝绱姞鍒扮粨鏋�</li>
+     * </ol>
+     *
+     * <p>杩欐牱閬垮厤浜嗘妸涓棿宸ュ簭鐨勬姇鏂欑洿鎺ョ撼鍏ヨ捣鐐瑰鑷寸殑閲嶅璁$畻闂銆�</p>
+     *
+     * @param processList 宸ュ崟宸ュ簭鍒楄〃锛堟寜 sort 鍗囧簭锛�
+     * @param bomList     宸ュ崟 BOM 琛岋紙鎵�鏈夊伐搴忕殑鎶曟枡鏄庣粏锛�
+     * @param routeId     宸ヨ壓璺嚎缂栧彿锛堝彲涓� null锛宯ull 鏃惰烦杩囪矾鐢� BOM 鏌ヨ锛�
+     * @return 鍙跺瓙鐗╂枡 Map锛歩temId 鈫� 闇�姹傛暟閲�
      */
-    private Map<Long, BigDecimal> buildWorkOrderItems(List<MesProWorkOrderBomDO> bomList) {
-        // 1. 鍒濆鍖栵細鏀堕泦绗竴灞傜墿鏂� ID 鍜屽搴旀暟閲�
-        Map<Long, BigDecimal> currentLayer = new LinkedHashMap<>();
-        for (MesProWorkOrderBomDO bom : bomList) {
-            currentLayer.merge(bom.getItemId(), bom.getQuantity(), BigDecimal::add);
+    private Map<Long, BigDecimal> buildWorkOrderItems(List<MesProWorkOrderProcessDO> processList,
+                                                       List<MesProWorkOrderBomDO> bomList,
+                                                       Long routeId) {
+        // 1. 鏋勫缓浜у嚭鍝� 鈫� 宸ュ簭鎶曟枡閰嶆柟锛坧er-unit锛�
+        //    outputItemId 鈫� [{itemId, quantity}, ...]
+        Map<Long, List<MesProWorkOrderBomDO>> processBomMap = new HashMap<>();
+        for (MesProWorkOrderProcessDO process : processList) {
+            if (process.getOutputItemId() == null) {
+                continue;
+            }
+            List<MesProWorkOrderBomDO> items = new ArrayList<>();
+            for (MesProWorkOrderBomDO bom : bomList) {
+                if (bom.getProcessId().equals(process.getProcessId())) {
+                    items.add(bom);
+                }
+            }
+            processBomMap.put(process.getOutputItemId(), items);
         }
 
-        // 2. 閫愬眰 IN 鏌ヨ灞曞紑 BOM
+        // 2. 鍙栨渶鍚庝竴閬撳伐搴忥紙sort 鏈�澶э級浣滀负灞曞紑璧风偣锛岄�愬眰鍚戝墠閫掓帹
+        MesProWorkOrderProcessDO lastProcess = processList.stream()
+                .max(Comparator.comparing(MesProWorkOrderProcessDO::getSort))
+                .orElse(null);
+        if (lastProcess == null) {
+            return Collections.emptyMap();
+        }
+
+        // 3. 浠ユ渶鍚庝竴閬撳伐搴忕殑鎶曟枡涓鸿捣鐐�
+        Map<Long, BigDecimal> currentLayer = new LinkedHashMap<>();
+        for (MesProWorkOrderBomDO bom : bomList) {
+            if (bom.getProcessId().equals(lastProcess.getProcessId())) {
+                currentLayer.merge(bom.getItemId(), bom.getQuantity(), BigDecimal::add);
+            }
+        }
+        if (CollUtil.isEmpty(currentLayer)) {
+            return Collections.emptyMap();
+        }
+
+        // 4. 閫愬眰灞曞紑
         Map<Long, BigDecimal> leafItems = new LinkedHashMap<>();
         for (int i = 0; i < Byte.MAX_VALUE && CollUtil.isNotEmpty(currentLayer); i++) {
-            // 2.1 鎵归噺鏌ヨ褰撳墠灞傛墍鏈夌墿鏂欑殑瀛� BOM
-            List<MesMdProductBomDO> subBomList = productBomService.getProductBomListByItemIds(currentLayer.keySet());
-            // 2.2 鎸� itemId 鍒嗙粍
-            Map<Long, List<MesMdProductBomDO>> subBomMap = convertMultiMap(subBomList, MesMdProductBomDO::getItemId);
-            // 2.3 鍖哄垎鍙跺瓙鑺傜偣鍜岄潪鍙跺瓙鑺傜偣
             Map<Long, BigDecimal> nextLayer = new LinkedHashMap<>();
+
+            // 4.1 鎵归噺鏌ヨ璺敱浜у搧 BOM锛堢敤浜庨潪宸ュ簭浜у嚭鐨勭墿鏂欏睍寮�锛�
+            Map<Long, List<MesProRouteProductBomDO>> routeSubBomMap;
+            if (routeId != null) {
+                List<MesProRouteProductBomDO> routeSubBomList = routeProductBomService
+                        .getRouteProductBomListByProductIds(routeId, currentLayer.keySet());
+                routeSubBomMap = convertMultiMap(routeSubBomList, MesProRouteProductBomDO::getProductId);
+            } else {
+                routeSubBomMap = Collections.emptyMap();
+            }
+
             for (Map.Entry<Long, BigDecimal> entry : currentLayer.entrySet()) {
                 Long itemId = entry.getKey();
-                BigDecimal quantity = entry.getValue();
-                List<MesMdProductBomDO> children = subBomMap.get(itemId);
-                // 鎯呭喌涓�锛氬彾瀛愯妭鐐癸紝绱姞鍒扮粨鏋�
-                if (CollUtil.isEmpty(children)) {
-                    leafItems.merge(itemId, quantity, BigDecimal::add);
+                BigDecimal qty = entry.getValue();
+
+                // 鎯呭喌涓�锛氱墿鏂欐槸鏌愬伐搴忕殑浜у嚭鍝� 鈫� 鐢ㄥ伐搴忛厤鏂规浛浠�
+                List<MesProWorkOrderBomDO> processBom = processBomMap.get(itemId);
+                if (CollUtil.isNotEmpty(processBom)) {
+                    for (MesProWorkOrderBomDO child : processBom) {
+                        nextLayer.merge(child.getItemId(), qty.multiply(child.getQuantity()), BigDecimal::add);
+                    }
                     continue;
                 }
-                // 鎯呭喌浜岋細闈炲彾瀛愶紝瀛愮墿鏂欒繘鍏ヤ笅涓�灞�
-                for (MesMdProductBomDO child : children) {
-                    nextLayer.merge(child.getBomItemId(), quantity.multiply(child.getQuantity()), BigDecimal::add);
+
+                // 鎯呭喌浜岋細璺敱浜у搧 BOM 涓湁涓嬬骇 鈫� 灞曞紑
+                List<MesProRouteProductBomDO> routeChildren = routeSubBomMap.get(itemId);
+                if (CollUtil.isNotEmpty(routeChildren)) {
+                    for (MesProRouteProductBomDO child : routeChildren) {
+                        nextLayer.merge(child.getItemId(), qty.multiply(child.getQuantity()), BigDecimal::add);
+                    }
+                    continue;
                 }
+
+                // 鎯呭喌涓夛細鍙跺瓙鐗╂枡
+                leafItems.merge(itemId, qty, BigDecimal::add);
             }
             currentLayer = nextLayer;
         }
         return leafItems;
     }
 
+    // ==================== 鎷兼帴鐗╂枡闇�姹� VO锛堝惈搴撳瓨鏍¢獙锛� ====================
+
+    private List<MesProWorkOrderItemRespVO> buildItemRespVOList(Map<Long, BigDecimal> requirements,
+                                                                  Map<Long, BigDecimal> purchaseOrderQtyMap) {
+        // 1. 鎵归噺鑾峰彇鐗╂枡鍩虹淇℃伅
+        Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(requirements.keySet());
+        Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
+                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
+        Map<Long, MesMdItemTypeDO> itemTypeMap = itemTypeService.getItemTypeMap(
+                convertSet(itemMap.values(), MesMdItemDO::getItemTypeId));
+
+        // 2. 鎵归噺鏌ヨ搴撳瓨锛屾寜 itemId 姹囨�诲彲鐢ㄩ噺锛堝湪搴� - 鍐荤粨 - 鍗犵敤锛�
+        Map<Long, BigDecimal> stockMap = new HashMap<>();
+        List<MesWmMaterialStockDO> stockList = materialStockMapper.selectListByItemIds(requirements.keySet());
+        for (MesWmMaterialStockDO stock : stockList) {
+            BigDecimal frozen = stock.getFrozenQuantity() != null ? stock.getFrozenQuantity() : BigDecimal.ZERO;
+            BigDecimal reserved = stock.getReservedQuantity() != null ? stock.getReservedQuantity() : BigDecimal.ZERO;
+            BigDecimal available = stock.getQuantity().subtract(frozen).subtract(reserved);
+            if (available.compareTo(BigDecimal.ZERO) > 0) {
+                stockMap.merge(stock.getItemId(), available, BigDecimal::add);
+            }
+        }
+
+        // 3. 鎷兼帴 VO
+        List<MesProWorkOrderItemRespVO> result = new ArrayList<>(requirements.size());
+        for (Map.Entry<Long, BigDecimal> entry : requirements.entrySet()) {
+            MesMdItemDO item = itemMap.get(entry.getKey());
+            if (item == null) {
+                continue;
+            }
+            BigDecimal requiredQty = entry.getValue();
+            BigDecimal stockQty = stockMap.getOrDefault(item.getId(), BigDecimal.ZERO);
+            BigDecimal purchaseQty = purchaseOrderQtyMap.getOrDefault(item.getId(), BigDecimal.ZERO);
+
+            // 鍏呰冻鍒ゆ柇 = 鍙敤搴撳瓨 + 鍦ㄩ�旈噰璐暟 >= 闇�姹傛暟閲�
+            BigDecimal totalSupply = stockQty.add(purchaseQty);
+
+            MesProWorkOrderItemRespVO vo = new MesProWorkOrderItemRespVO()
+                    .setItemId(item.getId()).setQuantity(requiredQty)
+                    .setItemCode(item.getCode()).setItemName(item.getName()).setItemSpecification(item.getSpecification())
+                    .setStockQuantity(stockQty)
+                    .setPurchaseOrderQuantity(purchaseQty)
+                    .setStockSufficient(totalSupply.compareTo(requiredQty) >= 0);
+            MapUtils.findAndThen(unitMeasureMap, item.getUnitMeasureId(),
+                    unitMeasure -> vo.setUnitMeasureName(unitMeasure.getName()));
+            MapUtils.findAndThen(itemTypeMap, item.getItemTypeId(),
+                    itemType -> vo.setItemOrProduct(itemType.getItemOrProduct()));
+            result.add(vo);
+        }
+        return result;
+    }
+
 }

--
Gitblit v1.9.3