云
17 小时以前 ac2e2b5af3726ee64dc37c3ebf26989a0346f22a
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderBomController.java
@@ -13,18 +13,23 @@
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.ErpPurchaseOrderApi;
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;
@@ -40,6 +45,7 @@
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;
@@ -51,6 +57,9 @@
@RequestMapping("/mes/pro/work-order-bom")
@Validated
public class MesProWorkOrderBomController {
    @Resource
    private MesProTaskService taskService;
    @Resource
    private MesProWorkOrderBomService workOrderBomService;
@@ -69,9 +78,11 @@
    @Resource
    private MesProRouteProductBomService routeProductBomService;
    @Resource
    private MesProRouteProcessService routeProcessService;
    @Resource
    private MesWmMaterialStockMapper materialStockMapper;
    @Resource
    private ErpPurchaseOrderApi purchaseOrderApi;
    private PurchaseOrderApi purchaseOrderApi;
    @PostMapping("/create")
    @Operation(summary = "创建工单 BOM")
@@ -100,7 +111,6 @@
    @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) {
@@ -111,16 +121,26 @@
    @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. 获取工单
@@ -153,7 +173,7 @@
        }
        // 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());
        }
@@ -230,29 +250,24 @@
     *
     * @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(process.getOutputItemId(), 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);
@@ -260,7 +275,7 @@
            return Collections.emptyMap();
        }
        // 3. 以最后一道工序的投料为起点
        // 3.1 以最后一道工序的投料为起点
        Map<Long, BigDecimal> currentLayer = new LinkedHashMap<>();
        for (MesProWorkOrderBomDO bom : bomList) {
            if (bom.getProcessId().equals(lastProcess.getProcessId())) {
@@ -275,8 +290,10 @@
        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
@@ -290,12 +307,10 @@
                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;
                }
@@ -311,11 +326,86 @@
                // 情况三:叶子物料
                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,