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 ====================
@@ -321,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<>();
@@ -355,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);