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.HashSet;
|
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<MesProWorkOrderItemRespVO> getItemListByProduct(Long productId, BigDecimal quantity) {
|
Map<Long, BigDecimal> perUnitItems = expandLeafItems(productId);
|
if (CollUtil.isEmpty(perUnitItems)) {
|
return Collections.emptyList();
|
}
|
BigDecimal productQuantity = quantity != null ? quantity : BigDecimal.ONE;
|
Map<Long, BigDecimal> requirements = new LinkedHashMap<>();
|
perUnitItems.forEach((itemId, perUnit) ->
|
requirements.put(itemId, perUnit.multiply(productQuantity)));
|
return buildItemRespVOList(requirements, Collections.emptyMap());
|
}
|
|
@Override
|
public Map<Long, BigDecimal> 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<Long, BigDecimal> currentLayer = new LinkedHashMap<>();
|
currentLayer.put(productId, BigDecimal.ONE);
|
Map<Long, BigDecimal> leafItems = new LinkedHashMap<>();
|
// 2. 逐层展开
|
for (int depth = 0; depth < MAX_BOM_DEPTH && CollUtil.isNotEmpty(currentLayer); depth++) {
|
Map<Long, List<MesProRouteProductBomDO>> routeBomMap = routeId != null
|
? convertMultiMap(routeProductBomService.getRouteProductBomListByProductIds(routeId, currentLayer.keySet()),
|
MesProRouteProductBomDO::getProductId)
|
: Collections.emptyMap();
|
Map<Long, List<MesMdProductBomDO>> productBomMap = convertMultiMap(
|
productBomService.getProductBomListByItemIds(currentLayer.keySet()), MesMdProductBomDO::getItemId);
|
|
Map<Long, BigDecimal> nextLayer = new LinkedHashMap<>();
|
for (Map.Entry<Long, BigDecimal> entry : currentLayer.entrySet()) {
|
BigDecimal qty = entry.getValue();
|
List<MesProRouteProductBomDO> 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<MesMdProductBomDO> 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<MesProWorkOrderItemRespVO> buildItemRespVOList(Map<Long, BigDecimal> requirements,
|
Map<Long, BigDecimal> purchaseOrderQtyMap) {
|
if (CollUtil.isEmpty(requirements)) {
|
return Collections.emptyList();
|
}
|
// 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 汇总可用量(在库 - 冻结 - 占用)
|
// 库存表存的是 mdm_item_id,而 BOM 展开得到的是 mes_md_item.id,需要先换算再查库存
|
Map<Long, BigDecimal> stockMap = new HashMap<>();
|
Map<Long, Long> mesToMdmItemId = new HashMap<>();
|
for (MesMdItemDO item : itemMap.values()) {
|
if (item.getMdmItemId() != null) {
|
mesToMdmItemId.put(item.getId(), item.getMdmItemId());
|
}
|
}
|
List<MesWmMaterialStockDO> stockList = materialStockMapper.selectListByItemIds(
|
new HashSet<>(mesToMdmItemId.values()));
|
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<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(
|
mesToMdmItemId.getOrDefault(item.getId(), 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;
|
}
|
|
}
|