liyong
5 天以前 3acb4bda764d615bc69d18339921cc92e0024cdd
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:itemId → 合并后的需求数量
     * <p><b>核心逻辑:</b></p>
     * <ol>
     *   <li>找出关键工序(keyFlag=true),以它的投料为展开起点</li>
     *   <li>构建产出品→工序投料的映射({@code outputItemId → per-unit bomItems})</li>
     *   <li>逐层展开:若物料是某工序的产出品 → 用该工序的投料配方替代;
     *       否则查路由产品 BOM → 用 BOM 子项替代;
     *       两者都没有 → 叶子物料(外购件),累加到结果</li>
     * </ol>
     *
     * <p>这样避免了把中间工序的投料直接纳入起点导致的重复计算问题。</p>
     *
     * @param processList 工单工序列表(按 sort 升序)
     * @param bomList     工单 BOM 行(所有工序的投料明细)
     * @param routeId     工艺路线编号(可为 null,null 时跳过路由 BOM 查询)
     * @return 叶子物料 Map:itemId → 需求数量
     */
    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. 构建产出品 → 工序投料配方(per-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;
    }
}