package cn.iocoder.yudao.module.mes.service.pro.material; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.util.collection.MapUtils; import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderItemRespVO; 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.wm.materialstock.MesWmMaterialStockDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper; 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 jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; /** * MES 物料需求(BOM 展开)Service 实现类 * * @author 超级管理员 */ @Service @Validated @Slf4j public class MesProMaterialRequirementServiceImpl implements MesProMaterialRequirementService { /** * BOM 最大展开层数,防止循环引用 */ private static final int MAX_BOM_DEPTH = 20; @Resource private MesMdItemService itemService; @Resource private MesMdItemTypeService itemTypeService; @Resource private MesMdUnitMeasureService unitMeasureService; @Resource private MesMdProductBomService productBomService; @Resource private MesProRouteProductService routeProductService; @Resource private MesProRouteProductBomService routeProductBomService; @Resource private MesWmMaterialStockMapper materialStockMapper; @Override public List getItemListByProduct(Long productId, BigDecimal quantity) { Map perUnitItems = expandLeafItems(productId); if (CollUtil.isEmpty(perUnitItems)) { return Collections.emptyList(); } BigDecimal productQuantity = quantity != null ? quantity : BigDecimal.ONE; Map requirements = new LinkedHashMap<>(); perUnitItems.forEach((itemId, perUnit) -> requirements.put(itemId, perUnit.multiply(productQuantity))); return buildItemRespVOList(requirements, Collections.emptyMap()); } @Override public Map expandLeafItems(Long productId) { if (productId == null) { return Collections.emptyMap(); } // 1. 反查产品对应的工艺路线(无路线时仅使用产品 BOM) Long routeId = null; MesProRouteProductDO routeProduct = routeProductService.getRouteProductByItemId(productId); if (routeProduct != null) { routeId = routeProduct.getRouteId(); } Map currentLayer = new LinkedHashMap<>(); currentLayer.put(productId, BigDecimal.ONE); Map leafItems = new LinkedHashMap<>(); // 2. 逐层展开 for (int depth = 0; depth < MAX_BOM_DEPTH && CollUtil.isNotEmpty(currentLayer); depth++) { Map> routeBomMap = routeId != null ? convertMultiMap(routeProductBomService.getRouteProductBomListByProductIds(routeId, currentLayer.keySet()), MesProRouteProductBomDO::getProductId) : Collections.emptyMap(); Map> productBomMap = convertMultiMap( productBomService.getProductBomListByItemIds(currentLayer.keySet()), MesMdProductBomDO::getItemId); Map nextLayer = new LinkedHashMap<>(); for (Map.Entry entry : currentLayer.entrySet()) { BigDecimal qty = entry.getValue(); List routeChildren = routeBomMap.get(entry.getKey()); if (CollUtil.isNotEmpty(routeChildren)) { for (MesProRouteProductBomDO child : routeChildren) { nextLayer.merge(child.getItemId(), qty.multiply(nullToZero(child.getQuantity())), BigDecimal::add); } continue; } List productChildren = productBomMap.get(entry.getKey()); if (CollUtil.isNotEmpty(productChildren)) { for (MesMdProductBomDO child : productChildren) { nextLayer.merge(child.getBomItemId(), qty.multiply(nullToZero(child.getQuantity())), BigDecimal::add); } continue; } // 顶级产品没有任何下级 BOM 时,说明尚未维护 BOM,不能把产品自身当成辅料 if (entry.getKey().equals(productId)) { continue; } // 无下级 BOM,视为叶子物料(外购件) leafItems.merge(entry.getKey(), qty, BigDecimal::add); } currentLayer = nextLayer; } return leafItems; } /** * 拼接物料需求 VO(含物料基础信息、可用库存、是否充足) */ private List buildItemRespVOList(Map requirements, Map purchaseOrderQtyMap) { if (CollUtil.isEmpty(requirements)) { return Collections.emptyList(); } // 1. 批量获取物料基础信息 Map itemMap = itemService.getItemMap(requirements.keySet()); Map unitMeasureMap = unitMeasureService.getUnitMeasureMap( convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId)); Map itemTypeMap = itemTypeService.getItemTypeMap( convertSet(itemMap.values(), MesMdItemDO::getItemTypeId)); // 2. 批量查询库存,按 itemId 汇总可用量(在库 - 冻结 - 占用) Map stockMap = new HashMap<>(); List stockList = materialStockMapper.selectListByItemIds(requirements.keySet()); for (MesWmMaterialStockDO stock : stockList) { BigDecimal available = nullToZero(stock.getQuantity()) .subtract(nullToZero(stock.getFrozenQuantity())) .subtract(nullToZero(stock.getReservedQuantity())); if (available.compareTo(BigDecimal.ZERO) > 0) { stockMap.merge(stock.getItemId(), available, BigDecimal::add); } } // 3. 拼接 VO List result = new ArrayList<>(requirements.size()); for (Map.Entry 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); MesProWorkOrderItemRespVO vo = new MesProWorkOrderItemRespVO() .setItemId(item.getId()).setQuantity(requiredQty) .setItemCode(item.getCode()).setItemName(item.getName()) .setItemSpecification(item.getSpecification()) .setStockQuantity(stockQty) .setPurchaseOrderQuantity(purchaseQty) .setStockSufficient(stockQty.add(purchaseQty).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; } private static BigDecimal nullToZero(BigDecimal value) { return value != null ? value : BigDecimal.ZERO; } }