| | |
| | | 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; |
| | |
| | | |
| | | 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; |
| | |
| | | @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") |
| | |
| | | @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 ==================== |
| | |
| | | }); |
| | | } |
| | | |
| | | // ==================== 物料需求逐层展开 ==================== |
| | | // ==================== 物料需求展开(工序产出链 + 路由产品 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; |
| | | } |
| | | |
| | | } |