From b3fe06c151dc8455649bf0a46e481ceb83e3ea2a Mon Sep 17 00:00:00 2001
From: 吴羡 <16976746+Xian-TT3@user.noreply.gitee.com>
Date: 星期六, 07 十一月 2026 13:37:54 +0800
Subject: [PATCH] feat: 完成好水川 MOM 需求1/2/4/5/8(装箱、BOM成本、销售合同、劳动合同、留样)

---
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderBomController.java |  155 ++++++++++++++++++++++++++++++++++++++-------------
 1 files changed, 114 insertions(+), 41 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 a44b286..f2f0d4e 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,19 +12,25 @@
 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.itemconsume.MesWmItemConsumeLineDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemconsume.MesWmItemConsumeLineMapper;
 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.cost.MesProMaterialPriceService;
+import cn.iocoder.yudao.module.mes.service.pro.cost.dto.MesProMaterialPriceRespDTO;
 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;
@@ -71,6 +77,12 @@
     @Resource
     private MesWmMaterialStockMapper materialStockMapper;
     @Resource
+    private MesWmItemConsumeLineMapper consumeLineMapper;
+    @Resource
+    private MesMdProductBomService productBomService;
+    @Resource
+    private MesProMaterialPriceService materialPriceService;
+    @Resource
     private ErpPurchaseOrderApi purchaseOrderApi;
 
     @PostMapping("/create")
@@ -100,7 +112,6 @@
     @GetMapping("/get")
     @Operation(summary = "鑾峰緱宸ュ崟 BOM")
     @Parameter(name = "id", description = "缂栧彿", required = true, example = "1024")
-    @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')")
     public CommonResult<MesProWorkOrderBomRespVO> getWorkOrderBom(@RequestParam("id") Long id) {
         MesProWorkOrderBomDO workOrderBom = workOrderBomService.getWorkOrderBom(id);
         if (workOrderBom == null) {
@@ -111,7 +122,6 @@
 
     @GetMapping("/page")
     @Operation(summary = "鑾峰緱宸ュ崟 BOM 鍒嗛〉")
-    @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')")
     public CommonResult<PageResult<MesProWorkOrderBomRespVO>> getWorkOrderBomPage(@Valid MesProWorkOrderBomPageReqVO pageReqVO) {
         PageResult<MesProWorkOrderBomDO> pageResult = workOrderBomService.getWorkOrderBomPage(pageReqVO);
         return success(new PageResult<>(buildWorkOrderBomRespVOList(pageResult.getList()), pageResult.getTotal()));
@@ -120,7 +130,6 @@
     @GetMapping("/item-list-by-work-order-id")
     @Operation(summary = "鑾峰緱宸ュ崟鐗╂枡闇�姹傚垪琛�")
     @Parameter(name = "workOrderId", description = "宸ュ崟缂栧彿", required = true)
-    @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')")
     public CommonResult<List<MesProWorkOrderItemRespVO>> getWorkOrderBomItemListByWorkOrderId(
             @RequestParam("workOrderId") Long workOrderId) {
         // 1. 鑾峰彇宸ュ崟
@@ -129,43 +138,18 @@
             return success(Collections.emptyList());
         }
 
-        // 2. 鑾峰彇宸ュ崟宸ュ簭鍒楄〃锛堝惈鍚勫伐搴忕殑鎶曟枡鏄庣粏涓庝骇鍑哄搧锛�
+        // 2. 鑾峰彇宸ュ崟宸ュ簭鍒楄〃涓庡伐鍗� BOM
         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)) {
+
+        // 3. 璁$畻鏈鐢熶骇鐨勬�荤墿鏂欓渶姹�
+        Map<Long, BigDecimal> totalRequirements = buildTotalRequirements(workOrder, processList, bomList);
+        if (CollUtil.isEmpty(totalRequirements)) {
             return success(Collections.emptyList());
         }
 
-        // 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());
-        }
-
-        // 6. 涔樹互宸ュ崟鐢熶骇鏁伴噺锛屽緱鍒版�婚渶姹�
-        BigDecimal orderQty = workOrder.getQuantity() != null ? workOrder.getQuantity() : BigDecimal.ONE;
-        Map<Long, BigDecimal> totalRequirements = new LinkedHashMap<>();
-        for (Map.Entry<Long, BigDecimal> entry : leafItems.entrySet()) {
-            totalRequirements.put(entry.getKey(), entry.getValue().multiply(orderQty));
-        }
-
-        // 7. 鏌ヨ鏉ユ簮涓烘湰宸ュ崟鐨勯噰璐鍗曟槑缁嗭紝鎸変骇鍝佹眹鎬诲湪閫旈噰璐暟
+        // 4. 鏌ヨ鏉ユ簮涓烘湰宸ュ崟鐨勯噰璐鍗曟槑缁嗭紝鎸変骇鍝佹眹鎬诲湪閫旈噰璐暟
         //    sourceType=1 琛ㄧず鏉ユ簮绫诲瀷涓�"鐢熶骇宸ュ崟"
         Map<Long, BigDecimal> purchaseOrderQtyMap = new HashMap<>();
         List<ErpPurchaseOrderItemRespDTO> poItems = purchaseOrderApi
@@ -182,8 +166,83 @@
             }
         }
 
-        // 8. 鏌ヨ搴撳瓨锛屾嫾鎺� VO
-        return success(buildItemRespVOList(totalRequirements, purchaseOrderQtyMap));
+        // 5. 鏌ヨ鏈伐鍗曠疮璁℃秷鑰楋紝鎷兼帴 VO
+        Map<Long, BigDecimal> actualQuantityMap = buildActualQuantityMap(workOrderId);
+        return success(buildItemRespVOList(totalRequirements, purchaseOrderQtyMap, actualQuantityMap));
+    }
+
+    /**
+     * 璁$畻宸ュ崟鎬荤墿鏂欓渶姹傘��
+     *
+     * 浼樺厛绾э細
+     * 1. 宸ュ簭浜у嚭閾惧睍寮�锛堟湁宸ュ簭銆佹湁宸ュ崟 BOM 鏃讹級
+     * 2. 浜у搧 BOM 脳 宸ュ崟鏁伴噺锛堟湭閰嶇疆宸ュ簭/宸ュ崟 BOM 鏃剁殑鍏滃簳锛屼繚璇佽兘鎸夊崟閲忕畻鏂欙級
+     * 3. 宸ュ崟 BOM 鍏滃簳锛堟棤宸ュ簭琛屽凡鎸夊伐鍗曟暟閲忔斁澶э紱鏈夊伐搴忚涓哄崟浣嶇敤閲忥紝闇�涔樺伐鍗曟暟閲忥級
+     */
+    private Map<Long, BigDecimal> buildTotalRequirements(MesProWorkOrderDO workOrder,
+                                                         List<MesProWorkOrderProcessDO> processList,
+                                                         List<MesProWorkOrderBomDO> bomList) {
+        BigDecimal orderQty = workOrder.getQuantity() != null ? workOrder.getQuantity() : BigDecimal.ONE;
+        // 1. 宸ュ簭浜у嚭閾惧睍寮�
+        if (CollUtil.isNotEmpty(processList) && CollUtil.isNotEmpty(bomList)) {
+            Long routeId = null;
+            if (workOrder.getProductId() != null) {
+                MesProRouteProductDO routeProduct = routeProductService
+                        .getRouteProductByItemId(workOrder.getProductId());
+                if (routeProduct != null) {
+                    routeId = routeProduct.getRouteId();
+                }
+            }
+            Map<Long, BigDecimal> leafItems = buildWorkOrderItems(processList, bomList, routeId);
+            if (CollUtil.isNotEmpty(leafItems)) {
+                return multiplyQuantity(leafItems, orderQty);
+            }
+        }
+        // 2. 浜у搧 BOM 脳 宸ュ崟鏁伴噺
+        List<MesMdProductBomDO> productBomList = productBomService
+                .getProductBomListByItemId(workOrder.getProductId());
+        if (CollUtil.isNotEmpty(productBomList)) {
+            Map<Long, BigDecimal> requirements = new LinkedHashMap<>();
+            for (MesMdProductBomDO bom : productBomList) {
+                if (bom.getBomItemId() != null && bom.getQuantity() != null) {
+                    requirements.merge(bom.getBomItemId(),
+                            bom.getQuantity().multiply(orderQty), BigDecimal::add);
+                }
+            }
+            return requirements;
+        }
+        // 3. 宸ュ崟 BOM 鍏滃簳
+        if (CollUtil.isNotEmpty(bomList)) {
+            Map<Long, BigDecimal> requirements = new LinkedHashMap<>();
+            for (MesProWorkOrderBomDO bom : bomList) {
+                if (bom.getItemId() == null || bom.getQuantity() == null) {
+                    continue;
+                }
+                BigDecimal quantity = bom.getProcessId() == null
+                        ? bom.getQuantity() : bom.getQuantity().multiply(orderQty);
+                requirements.merge(bom.getItemId(), quantity, BigDecimal::add);
+            }
+            return requirements;
+        }
+        return Collections.emptyMap();
+    }
+
+    private static Map<Long, BigDecimal> multiplyQuantity(Map<Long, BigDecimal> source, BigDecimal factor) {
+        Map<Long, BigDecimal> result = new LinkedHashMap<>();
+        for (Map.Entry<Long, BigDecimal> entry : source.entrySet()) {
+            result.put(entry.getKey(), entry.getValue().multiply(factor));
+        }
+        return result;
+    }
+
+    private Map<Long, BigDecimal> buildActualQuantityMap(Long workOrderId) {
+        Map<Long, BigDecimal> result = new HashMap<>();
+        for (MesWmItemConsumeLineDO line : consumeLineMapper.selectListByWorkOrderId(workOrderId)) {
+            if (line.getItemId() != null && line.getQuantity() != null) {
+                result.merge(line.getItemId(), line.getQuantity(), BigDecimal::add);
+            }
+        }
+        return result;
     }
 
     // ==================== 鎷兼帴 VO ====================
@@ -245,11 +304,13 @@
             }
             List<MesProWorkOrderBomDO> items = new ArrayList<>();
             for (MesProWorkOrderBomDO bom : bomList) {
-                if (bom.getProcessId().equals(process.getProcessId())) {
+                // 璺宠繃鏃犲伐搴忕紪鍙风殑鎶曟枡琛岋紙宸ュ崟 BOM 涓粎鎸夊伐搴忕淮鎶ゆ姇鏂欐槑缁嗭紝鏃犲伐搴忕殑琛屼笉鍙備笌灞曞紑锛�
+                // 涓� getProcessId 鍙兘涓� null锛岀洿鎺� equals 浼� NPE锛�
+                if (Objects.equals(bom.getProcessId(), process.getProcessId())) {
                     items.add(bom);
                 }
             }
-            processBomMap.put(process.getOutputItemId(), items);
+            processBomMap.put(Long.valueOf(process.getOutputItemId().split(",")[0].trim()), items);
         }
 
         // 2. 鍙栨渶鍚庝竴閬撳伐搴忥紙sort 鏈�澶э級浣滀负灞曞紑璧风偣锛岄�愬眰鍚戝墠閫掓帹
@@ -263,7 +324,7 @@
         // 3. 浠ユ渶鍚庝竴閬撳伐搴忕殑鎶曟枡涓鸿捣鐐�
         Map<Long, BigDecimal> currentLayer = new LinkedHashMap<>();
         for (MesProWorkOrderBomDO bom : bomList) {
-            if (bom.getProcessId().equals(lastProcess.getProcessId())) {
+            if (Objects.equals(bom.getProcessId(), lastProcess.getProcessId())) {
                 currentLayer.merge(bom.getItemId(), bom.getQuantity(), BigDecimal::add);
             }
         }
@@ -319,13 +380,16 @@
     // ==================== 鎷兼帴鐗╂枡闇�姹� VO锛堝惈搴撳瓨鏍¢獙锛� ====================
 
     private List<MesProWorkOrderItemRespVO> buildItemRespVOList(Map<Long, BigDecimal> requirements,
-                                                                  Map<Long, BigDecimal> purchaseOrderQtyMap) {
-        // 1. 鎵归噺鑾峰彇鐗╂枡鍩虹淇℃伅
+                                                                  Map<Long, BigDecimal> purchaseOrderQtyMap,
+                                                                  Map<Long, BigDecimal> actualQuantityMap) {
+        // 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));
+        Map<Long, MesProMaterialPriceRespDTO> priceMap = materialPriceService
+                .getMaterialPriceMap(requirements.keySet());
 
         // 2. 鎵归噺鏌ヨ搴撳瓨锛屾寜 itemId 姹囨�诲彲鐢ㄩ噺锛堝湪搴� - 鍐荤粨 - 鍗犵敤锛�
         Map<Long, BigDecimal> stockMap = new HashMap<>();
@@ -353,9 +417,18 @@
             // 鍏呰冻鍒ゆ柇 = 鍙敤搴撳瓨 + 鍦ㄩ�旈噰璐暟 >= 闇�姹傛暟閲�
             BigDecimal totalSupply = stockQty.add(purchaseQty);
 
+            MesProMaterialPriceRespDTO price = priceMap.get(item.getId());
+            BigDecimal unitPrice = price != null && price.getUnitPrice() != null
+                    ? price.getUnitPrice() : BigDecimal.ZERO;
+            BigDecimal amount = requiredQty.multiply(unitPrice);
+
             MesProWorkOrderItemRespVO vo = new MesProWorkOrderItemRespVO()
                     .setItemId(item.getId()).setQuantity(requiredQty)
                     .setItemCode(item.getCode()).setItemName(item.getName()).setItemSpecification(item.getSpecification())
+                    .setActualQuantity(actualQuantityMap.getOrDefault(item.getId(), BigDecimal.ZERO))
+                    .setUnitPrice(unitPrice)
+                    .setAmount(amount)
+                    .setPriceMissing(price == null || Boolean.TRUE.equals(price.getPriceMissing()))
                     .setStockQuantity(stockQty)
                     .setPurchaseOrderQuantity(purchaseQty)
                     .setStockSufficient(totalSupply.compareTo(requiredQty) >= 0);

--
Gitblit v1.9.3