package cn.iocoder.yudao.module.mes.controller.admin.pro.workorder; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.ListUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.MapUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderItemRespVO; import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.bom.MesProWorkOrderBomPageReqVO; import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.bom.MesProWorkOrderBomRespVO; 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.unitmeasure.MesMdUnitMeasureDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessOutputDO; 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.pro.task.MesProTaskDO; import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService; 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.PurchaseOrderApi; 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.unitmeasure.MesMdUnitMeasureService; import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService; 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 jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; import java.math.RoundingMode; 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; @Tag(name = "管理后台 - MES 生产工单 BOM") @RestController @RequestMapping("/mes/pro/work-order-bom") @Validated public class MesProWorkOrderBomController { @Resource private MesProTaskService taskService; @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 MesProRouteProductService routeProductService; @Resource private MesProRouteProductBomService routeProductBomService; @Resource private MesProRouteProcessService routeProcessService; @Resource private MesWmMaterialStockMapper materialStockMapper; @Resource private PurchaseOrderApi purchaseOrderApi; @PostMapping("/create") @Operation(summary = "创建工单 BOM") @PreAuthorize("@ss.hasPermission('mes:pro-work-order:update')") public CommonResult createWorkOrderBom(@Valid @RequestBody MesProWorkOrderBomSaveReqVO createReqVO) { return success(workOrderBomService.createWorkOrderBom(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新工单 BOM") @PreAuthorize("@ss.hasPermission('mes:pro-work-order:update')") public CommonResult updateWorkOrderBom(@Valid @RequestBody MesProWorkOrderBomSaveReqVO updateReqVO) { workOrderBomService.updateWorkOrderBom(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除工单 BOM") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('mes:pro-work-order:update')") public CommonResult deleteWorkOrderBom(@RequestParam("id") Long id) { workOrderBomService.deleteWorkOrderBom(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得工单 BOM") @Parameter(name = "id", description = "编号", required = true, example = "1024") public CommonResult getWorkOrderBom(@RequestParam("id") Long id) { MesProWorkOrderBomDO workOrderBom = workOrderBomService.getWorkOrderBom(id); if (workOrderBom == null) { return success(null); } return success(buildWorkOrderBomRespVOList(ListUtil.of(workOrderBom)).get(0)); } @GetMapping("/page") @Operation(summary = "获得工单 BOM 分页") public CommonResult> getWorkOrderBomPage(@Valid MesProWorkOrderBomPageReqVO pageReqVO) { PageResult pageResult = workOrderBomService.getWorkOrderBomPage(pageReqVO); return success(new PageResult<>(buildWorkOrderBomRespVOList(pageResult.getList()), pageResult.getTotal())); } @GetMapping("/list-by-task-id") @Operation(summary = "获得生产任务工序的投入物料") public CommonResult> getWorkOrderBomListByTaskId(@RequestParam("taskId") Long taskId) { MesProTaskDO task = taskService.validateTaskExists(taskId); if (task.getWorkOrderId() == null || task.getProcessId() == null) { return success(Collections.emptyList()); } List list = workOrderBomService.getWorkOrderBomListByWorkOrderId(task.getWorkOrderId()) .stream().filter(bom -> Objects.equals(bom.getProcessId(), task.getProcessId())).toList(); return success(buildWorkOrderBomRespVOList(list)); } @GetMapping("/item-list-by-work-order-id") @Operation(summary = "获得工单物料需求列表") @Parameter(name = "workOrderId", description = "工单编号", required = true) public CommonResult> getWorkOrderBomItemListByWorkOrderId( @RequestParam("workOrderId") Long workOrderId) { // 1. 获取工单 MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(workOrderId); if (workOrder == null) { return success(Collections.emptyList()); } // 2. 获取工单工序列表(含各工序的投料明细与产出品) List processList = workOrderProcessService .getWorkOrderProcessListByWorkOrderId(workOrderId); if (CollUtil.isEmpty(processList)) { return success(Collections.emptyList()); } // 3. 获取工单 BOM 行(各工序的投料记录,按 processId 分组使用) List bomList = workOrderBomService.getWorkOrderBomListByWorkOrderId(workOrderId); if (CollUtil.isEmpty(bomList)) { 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 leafItems = buildWorkOrderItems(processList, bomList, routeId, workOrder.getProductId()); if (CollUtil.isEmpty(leafItems)) { return success(Collections.emptyList()); } // 6. 乘以工单生产数量,得到总需求 BigDecimal orderQty = workOrder.getQuantity() != null ? workOrder.getQuantity() : BigDecimal.ONE; Map totalRequirements = new LinkedHashMap<>(); for (Map.Entry entry : leafItems.entrySet()) { totalRequirements.put(entry.getKey(), entry.getValue().multiply(orderQty)); } // 7. 查询来源为本工单的采购订单明细,按产品汇总在途采购数 // sourceType=1 表示来源类型为"生产工单" Map purchaseOrderQtyMap = new HashMap<>(); List 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 ==================== private List buildWorkOrderBomRespVOList(List list) { if (CollUtil.isEmpty(list)) { return Collections.emptyList(); } // 1. 批量获取关联数据 Map itemMap = itemService.getItemMap( convertSet(list, MesProWorkOrderBomDO::getItemId)); Map unitMeasureMap = unitMeasureService.getUnitMeasureMap( convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId)); Map itemTypeMap = itemTypeService.getItemTypeMap( convertSet(itemMap.values(), MesMdItemDO::getItemTypeId)); // 2. 拼接 VO return BeanUtils.toBean(list, MesProWorkOrderBomRespVO.class, vo -> { MapUtils.findAndThen(itemMap, vo.getItemId(), item -> { vo.setItemName(item.getName()).setItemCode(item.getCode()) .setItemSpecification(item.getSpecification()); MapUtils.findAndThen(itemTypeMap, item.getItemTypeId(), itemType -> vo.setItemOrProduct(itemType.getItemOrProduct())); MapUtils.findAndThen(unitMeasureMap, item.getUnitMeasureId(), unitMeasure -> vo.setUnitMeasureName(unitMeasure.getName())); }); }); } // ==================== 物料需求展开(工序产出链 + 路由产品 BOM) ==================== /** * 按工序产出链逐层展开物料需求。 * *

核心逻辑:

*
    *
  1. 找出关键工序(keyFlag=true),以它的投料为展开起点
  2. *
  3. 构建产出品→工序投料的映射({@code outputItemId → per-unit bomItems})
  4. *
  5. 逐层展开:若物料是某工序的产出品 → 用该工序的投料配方替代; * 否则查路由产品 BOM → 用 BOM 子项替代; * 两者都没有 → 叶子物料(外购件),累加到结果
  6. *
* *

这样避免了把中间工序的投料直接纳入起点导致的重复计算问题。

* * @param processList 工单工序列表(按 sort 升序) * @param bomList 工单 BOM 行(所有工序的投料明细) * @param routeId 工艺路线编号(可为 null,null 时跳过路由 BOM 查询与配比回查) * @param productId 产品编号(用于回查该产品的产出配比) * @return 叶子物料 Map:itemId → 需求数量 */ private Map buildWorkOrderItems(List processList, List bomList, Long routeId, Long productId) { // 1. 工序投料配方:processId → 投料明细 Map> processBomMap = new HashMap<>(); for (MesProWorkOrderBomDO bom : bomList) { if (bom.getProcessId() != null) { processBomMap.computeIfAbsent(bom.getProcessId(), key -> new ArrayList<>()).add(bom); } } // 2. 产出物料 → 所属工序 + 每轮产出量(主产出按 1,联产出按路线配比) Map outputProducerMap = buildOutputProducers(processList, routeId, productId); // 3. 取最后一道工序(sort 最大)作为展开起点,逐层向前递推 MesProWorkOrderProcessDO lastProcess = processList.stream() .max(Comparator.comparing(MesProWorkOrderProcessDO::getSort)) .orElse(null); if (lastProcess == null) { return Collections.emptyMap(); } // 3.1 以最后一道工序的投料为起点 Map 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 leafItems = new LinkedHashMap<>(); for (int i = 0; i < Byte.MAX_VALUE && CollUtil.isNotEmpty(currentLayer); i++) { Map nextLayer = new LinkedHashMap<>(); // 4.1 同一工序的多个联产出先归并,再按轮次只展开一次,避免投料重复计算 Map>> processOutputs = new LinkedHashMap<>(); // 4.2 批量查询路由产品 BOM(用于非工序产出的物料展开) Map> routeSubBomMap; if (routeId != null) { List routeSubBomList = routeProductBomService .getRouteProductBomListByProductIds(routeId, currentLayer.keySet()); routeSubBomMap = convertMultiMap(routeSubBomList, MesProRouteProductBomDO::getProductId); } else { routeSubBomMap = Collections.emptyMap(); } for (Map.Entry entry : currentLayer.entrySet()) { Long itemId = entry.getKey(); BigDecimal qty = entry.getValue(); // 情况一:物料是某工序的产出品 → 记入该工序,待整层归并后展开 OutputProducer producer = outputProducerMap.get(itemId); if (producer != null && CollUtil.isNotEmpty(processBomMap.get(producer.processId()))) { processOutputs.computeIfAbsent(producer.processId(), key -> new ArrayList<>()).add(entry); continue; } // 情况二:路由产品 BOM 中有下级 → 展开 List 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); } // 情况一(续):轮次 = max(各产出需求量 / 该产出每轮产出量),用轮次展开一次该工序投料 for (Map.Entry>> entry : processOutputs.entrySet()) { BigDecimal runs = BigDecimal.ZERO; for (Map.Entry output : entry.getValue()) { OutputProducer producer = outputProducerMap.get(output.getKey()); BigDecimal perUnit = producer != null && producer.perUnit().signum() > 0 ? producer.perUnit() : BigDecimal.ONE; BigDecimal outputRuns = output.getValue().divide(perUnit, 10, RoundingMode.HALF_UP); if (outputRuns.compareTo(runs) > 0) { runs = outputRuns; } } for (MesProWorkOrderBomDO child : processBomMap.get(entry.getKey())) { nextLayer.merge(child.getItemId(), runs.multiply(child.getQuantity()), BigDecimal::add); } } currentLayer = nextLayer; } return leafItems; } /** * 构建产出物料 → 所属工序 + 每轮产出量。 * 工序产出快照的第一位为主产出(每轮产出 1),其余产出从路线配置回查配比,缺失按 1。 */ private Map buildOutputProducers(List processList, Long routeId, Long productId) { Map outputProducerMap = new HashMap<>(); if (CollUtil.isEmpty(processList)) { return outputProducerMap; } Map routeProcessIds = new HashMap<>(); if (routeId != null) { for (MesProRouteProcessDO routeProcess : routeProcessService.getRouteProcessListByRouteId(routeId)) { if (routeProcess.getProcessId() != null && routeProcess.getId() != null) { routeProcessIds.putIfAbsent(routeProcess.getProcessId(), routeProcess.getId()); } } } for (MesProWorkOrderProcessDO process : processList) { List outputIds = parseItemIds(process.getOutputItemId()); if (CollUtil.isEmpty(outputIds)) { continue; } Map ratioMap = new HashMap<>(); Long routeProcessId = routeProcessIds.get(process.getProcessId()); if (routeProcessId != null) { for (MesProRouteProcessOutputDO config : routeProcessService.getProcessOutputs(routeProcessId, productId)) { if (config.getItemId() != null && config.getQuantity() != null) { ratioMap.putIfAbsent(config.getItemId(), config.getQuantity()); } } } for (int i = 0; i < outputIds.size(); i++) { Long itemId = outputIds.get(i); BigDecimal perUnit = i == 0 ? BigDecimal.ONE : ratioMap.getOrDefault(itemId, BigDecimal.ONE); outputProducerMap.putIfAbsent(itemId, new OutputProducer(process.getProcessId(), perUnit)); } } return outputProducerMap; } private List parseItemIds(String csv) { if (csv == null || csv.isBlank()) { return Collections.emptyList(); } List itemIds = new ArrayList<>(); for (String part : csv.split(",")) { String value = part.trim(); if (!value.isEmpty()) { itemIds.add(Long.valueOf(value)); } } return itemIds; } private record OutputProducer(Long processId, BigDecimal perUnit) { } // ==================== 拼接物料需求 VO(含库存校验) ==================== private List buildItemRespVOList(Map requirements, Map purchaseOrderQtyMap) { // 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 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 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); // 充足判断 = 可用库存 + 在途采购数 >= 需求数量 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; } }