| | |
| | | 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.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 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; |
| | |
| | | @RequestMapping("/mes/pro/work-order-bom") |
| | | @Validated |
| | | public class MesProWorkOrderBomController { |
| | | |
| | | @Resource |
| | | private MesProTaskService taskService; |
| | | |
| | | @Resource |
| | | private MesProWorkOrderBomService workOrderBomService; |
| | |
| | | private MesProRouteProductService routeProductService; |
| | | @Resource |
| | | private MesProRouteProductBomService routeProductBomService; |
| | | @Resource |
| | | private MesProRouteProcessService routeProcessService; |
| | | @Resource |
| | | private MesWmMaterialStockMapper materialStockMapper; |
| | | @Resource |
| | |
| | | @GetMapping("/get") |
| | | @Operation(summary = "获得工单 BOM") |
| | | @Parameter(name = "id", description = "编号", required = true, example = "1024") |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')") |
| | | public CommonResult<MesProWorkOrderBomRespVO> getWorkOrderBom(@RequestParam("id") Long id) { |
| | | MesProWorkOrderBomDO workOrderBom = workOrderBomService.getWorkOrderBom(id); |
| | | if (workOrderBom == null) { |
| | |
| | | |
| | | @GetMapping("/page") |
| | | @Operation(summary = "获得工单 BOM 分页") |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')") |
| | | public CommonResult<PageResult<MesProWorkOrderBomRespVO>> getWorkOrderBomPage(@Valid MesProWorkOrderBomPageReqVO pageReqVO) { |
| | | PageResult<MesProWorkOrderBomDO> pageResult = workOrderBomService.getWorkOrderBomPage(pageReqVO); |
| | | return success(new PageResult<>(buildWorkOrderBomRespVOList(pageResult.getList()), pageResult.getTotal())); |
| | | } |
| | | |
| | | @GetMapping("/list-by-task-id") |
| | | @Operation(summary = "获得生产任务工序的投入物料") |
| | | public CommonResult<List<MesProWorkOrderBomRespVO>> getWorkOrderBomListByTaskId(@RequestParam("taskId") Long taskId) { |
| | | MesProTaskDO task = taskService.validateTaskExists(taskId); |
| | | if (task.getWorkOrderId() == null || task.getProcessId() == null) { |
| | | return success(Collections.emptyList()); |
| | | } |
| | | List<MesProWorkOrderBomDO> 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) |
| | | @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')") |
| | | public CommonResult<List<MesProWorkOrderItemRespVO>> getWorkOrderBomItemListByWorkOrderId( |
| | | @RequestParam("workOrderId") Long workOrderId) { |
| | | // 1. 获取工单 |
| | |
| | | } |
| | | |
| | | // 5. 按工序产出链逐层展开 BOM,得到叶子物料(每单位成品的用量) |
| | | Map<Long, BigDecimal> leafItems = buildWorkOrderItems(processList, bomList, routeId); |
| | | Map<Long, BigDecimal> leafItems = buildWorkOrderItems(processList, bomList, routeId, workOrder.getProductId()); |
| | | if (CollUtil.isEmpty(leafItems)) { |
| | | return success(Collections.emptyList()); |
| | | } |
| | |
| | | * |
| | | * @param processList 工单工序列表(按 sort 升序) |
| | | * @param bomList 工单 BOM 行(所有工序的投料明细) |
| | | * @param routeId 工艺路线编号(可为 null,null 时跳过路由 BOM 查询) |
| | | * @param routeId 工艺路线编号(可为 null,null 时跳过路由 BOM 查询与配比回查) |
| | | * @param productId 产品编号(用于回查该产品的产出配比) |
| | | * @return 叶子物料 Map:itemId → 需求数量 |
| | | */ |
| | | private Map<Long, BigDecimal> buildWorkOrderItems(List<MesProWorkOrderProcessDO> processList, |
| | | List<MesProWorkOrderBomDO> bomList, |
| | | Long routeId) { |
| | | // 1. 构建产出品 → 工序投料配方(per-unit) |
| | | // outputItemId → [{itemId, quantity}, ...] |
| | | Long routeId, Long productId) { |
| | | // 1. 工序投料配方:processId → 投料明细 |
| | | Map<Long, List<MesProWorkOrderBomDO>> processBomMap = new HashMap<>(); |
| | | for (MesProWorkOrderProcessDO process : processList) { |
| | | if (process.getOutputItemId() == null) { |
| | | continue; |
| | | for (MesProWorkOrderBomDO bom : bomList) { |
| | | if (bom.getProcessId() != null) { |
| | | processBomMap.computeIfAbsent(bom.getProcessId(), key -> new ArrayList<>()).add(bom); |
| | | } |
| | | List<MesProWorkOrderBomDO> items = new ArrayList<>(); |
| | | for (MesProWorkOrderBomDO bom : bomList) { |
| | | if (bom.getProcessId().equals(process.getProcessId())) { |
| | | items.add(bom); |
| | | } |
| | | } |
| | | processBomMap.put(Long.valueOf(process.getOutputItemId().split(",")[0].trim()), items); |
| | | } |
| | | // 2. 产出物料 → 所属工序 + 每轮产出量(主产出按 1,联产出按路线配比) |
| | | Map<Long, OutputProducer> outputProducerMap = buildOutputProducers(processList, routeId, productId); |
| | | |
| | | // 2. 取最后一道工序(sort 最大)作为展开起点,逐层向前递推 |
| | | // 3. 取最后一道工序(sort 最大)作为展开起点,逐层向前递推 |
| | | MesProWorkOrderProcessDO lastProcess = processList.stream() |
| | | .max(Comparator.comparing(MesProWorkOrderProcessDO::getSort)) |
| | | .orElse(null); |
| | |
| | | return Collections.emptyMap(); |
| | | } |
| | | |
| | | // 3. 以最后一道工序的投料为起点 |
| | | // 3.1 以最后一道工序的投料为起点 |
| | | Map<Long, BigDecimal> currentLayer = new LinkedHashMap<>(); |
| | | for (MesProWorkOrderBomDO bom : bomList) { |
| | | if (bom.getProcessId().equals(lastProcess.getProcessId())) { |
| | |
| | | Map<Long, BigDecimal> leafItems = new LinkedHashMap<>(); |
| | | for (int i = 0; i < Byte.MAX_VALUE && CollUtil.isNotEmpty(currentLayer); i++) { |
| | | Map<Long, BigDecimal> nextLayer = new LinkedHashMap<>(); |
| | | // 4.1 同一工序的多个联产出先归并,再按轮次只展开一次,避免投料重复计算 |
| | | Map<Long, List<Map.Entry<Long, BigDecimal>>> processOutputs = new LinkedHashMap<>(); |
| | | |
| | | // 4.1 批量查询路由产品 BOM(用于非工序产出的物料展开) |
| | | // 4.2 批量查询路由产品 BOM(用于非工序产出的物料展开) |
| | | Map<Long, List<MesProRouteProductBomDO>> routeSubBomMap; |
| | | if (routeId != null) { |
| | | List<MesProRouteProductBomDO> routeSubBomList = routeProductBomService |
| | |
| | | Long itemId = entry.getKey(); |
| | | 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); |
| | | } |
| | | // 情况一:物料是某工序的产出品 → 记入该工序,待整层归并后展开 |
| | | OutputProducer producer = outputProducerMap.get(itemId); |
| | | if (producer != null && CollUtil.isNotEmpty(processBomMap.get(producer.processId()))) { |
| | | processOutputs.computeIfAbsent(producer.processId(), key -> new ArrayList<>()).add(entry); |
| | | continue; |
| | | } |
| | | |
| | |
| | | // 情况三:叶子物料 |
| | | leafItems.merge(itemId, qty, BigDecimal::add); |
| | | } |
| | | |
| | | // 情况一(续):轮次 = max(各产出需求量 / 该产出每轮产出量),用轮次展开一次该工序投料 |
| | | for (Map.Entry<Long, List<Map.Entry<Long, BigDecimal>>> entry : processOutputs.entrySet()) { |
| | | BigDecimal runs = BigDecimal.ZERO; |
| | | for (Map.Entry<Long, BigDecimal> 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<Long, OutputProducer> buildOutputProducers(List<MesProWorkOrderProcessDO> processList, |
| | | Long routeId, Long productId) { |
| | | Map<Long, OutputProducer> outputProducerMap = new HashMap<>(); |
| | | if (CollUtil.isEmpty(processList)) { |
| | | return outputProducerMap; |
| | | } |
| | | Map<Long, Long> 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<Long> outputIds = parseItemIds(process.getOutputItemId()); |
| | | if (CollUtil.isEmpty(outputIds)) { |
| | | continue; |
| | | } |
| | | Map<Long, BigDecimal> 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<Long> parseItemIds(String csv) { |
| | | if (csv == null || csv.isBlank()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | List<Long> 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<MesProWorkOrderItemRespVO> buildItemRespVOList(Map<Long, BigDecimal> requirements, |