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.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.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 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.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 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 MesWmMaterialStockMapper materialStockMapper; @Resource private ErpPurchaseOrderApi 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") @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')") 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 分页") @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')") public CommonResult> getWorkOrderBomPage(@Valid MesProWorkOrderBomPageReqVO pageReqVO) { PageResult pageResult = workOrderBomService.getWorkOrderBomPage(pageReqVO); return success(new PageResult<>(buildWorkOrderBomRespVOList(pageResult.getList()), pageResult.getTotal())); } @GetMapping("/item-list-by-work-order-id") @Operation(summary = "获得工单物料需求列表") @Parameter(name = "workOrderId", description = "工单编号", required = true) @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')") 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); 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 查询) * @return 叶子物料 Map:itemId → 需求数量 */ private Map buildWorkOrderItems(List processList, List bomList, Long routeId) { // 1. 构建产出品 → 工序投料配方(per-unit) // outputItemId → [{itemId, quantity}, ...] Map> processBomMap = new HashMap<>(); for (MesProWorkOrderProcessDO process : processList) { if (process.getOutputItemId() == null) { continue; } List items = new ArrayList<>(); for (MesProWorkOrderBomDO bom : bomList) { if (bom.getProcessId().equals(process.getProcessId())) { items.add(bom); } } processBomMap.put(process.getOutputItemId(), items); } // 2. 取最后一道工序(sort 最大)作为展开起点,逐层向前递推 MesProWorkOrderProcessDO lastProcess = processList.stream() .max(Comparator.comparing(MesProWorkOrderProcessDO::getSort)) .orElse(null); if (lastProcess == null) { return Collections.emptyMap(); } // 3. 以最后一道工序的投料为起点 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 批量查询路由产品 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(); // 情况一:物料是某工序的产出品 → 用工序配方替代 List processBom = processBomMap.get(itemId); if (CollUtil.isNotEmpty(processBom)) { for (MesProWorkOrderBomDO child : processBom) { nextLayer.merge(child.getItemId(), qty.multiply(child.getQuantity()), BigDecimal::add); } 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); } currentLayer = nextLayer; } return leafItems; } // ==================== 拼接物料需求 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; } }