gongchunyi
7 小时以前 a8188d1675594d7756eddccc3eda60601325942a
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -34,6 +34,7 @@
import com.ruoyi.quality.pojo.QualityInspectFile;
import com.ruoyi.quality.pojo.QualityInspectParam;
import com.ruoyi.production.service.ProductionOrderService;
import com.ruoyi.production.util.TaskPlanQuantityUtil;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.stock.mapper.StockInventoryMapper;
@@ -250,7 +251,7 @@
                        .eq(TechnologyRoutingOperation::getTechnologyRoutingId, technologyRouting.getId())
                        .orderByDesc(TechnologyRoutingOperation::getDragSort)
                        .orderByDesc(TechnologyRoutingOperation::getId));
        // Build task plan quantities from order BOM snapshot demand instead of recomputing from technology BOM units.
        // 工单计划数量直接使用订单 BOM 快照中的需求量,避免与 BOM 页面展示口径不一致。
        Long rootProductModelId = orderBom != null && orderBom.getProductModelId() != null
                ? orderBom.getProductModelId()
                : productionOrder.getProductModelId();
@@ -261,7 +262,9 @@
                        .eq(ProductionBomStructure::getProductionOrderBomId, orderBom.getId())
                        .orderByAsc(ProductionBomStructure::getId));
        Map<String, BigDecimal> operationDemandedQuantityMap =
                buildOperationDemandedQuantityMap(orderBomStructureList, rootProductModelId);
                TaskPlanQuantityUtil.buildOperationDemandedQuantityMap(orderBomStructureList, rootProductModelId);
        Map<Long, Set<Long>> operationOutputProductModelMap =
                TaskPlanQuantityUtil.buildOperationOutputProductModelMap(orderBomStructureList, rootProductModelId);
        Map<Long, String> operationNameMap = technologyOperationMapper.selectBatchIds(
                        // 遍历处理数据并组装结果
                        routingOperations.stream()
@@ -281,7 +284,12 @@
            targetOperation.setProductionOrderId(productionOrder.getId());
            targetOperation.setTechnologyRoutingOperationId(sourceOperation.getId());
            targetOperation.setOrderRoutingId(orderRouting.getId());
            targetOperation.setProductModelId(sourceOperation.getProductModelId());
            Long outputProductModelId = TaskPlanQuantityUtil.resolveOperationOutputProductModelId(
                    sourceOperation.getTechnologyOperationId(),
                    sourceOperation.getProductModelId(),
                    operationOutputProductModelMap,
                    rootProductModelId);
            targetOperation.setProductModelId(outputProductModelId);
            targetOperation.setDragSort(sourceOperation.getDragSort());
            targetOperation.setIsProduction(sourceOperation.getIsProduction());
            targetOperation.setIsQuality(sourceOperation.getIsQuality());
@@ -295,10 +303,10 @@
                ProductionOperationTask task = new ProductionOperationTask();
                task.setProductionOrderRoutingOperationId(targetOperation.getId());
                task.setProductionOrderId(productionOrder.getId());
                task.setPlanQuantity(resolveTaskPlanQuantity(
                        sourceOperation,
                task.setPlanQuantity(TaskPlanQuantityUtil.resolveTaskPlanQuantity(
                        targetOperation,
                        operationDemandedQuantityMap,
                        productionOrder,
                        productionOrder.getQuantity(),
                        rootProductModelId));
                task.setCompleteQuantity(BigDecimal.ZERO);
                task.setWorkOrderNo(generateNextTaskNo());
@@ -334,85 +342,6 @@
        return syncedParamCount;
    }
    private Map<String, BigDecimal> buildOperationDemandedQuantityMap(List<ProductionBomStructure> bomStructures,
                                                                      Long rootProductModelId) {
        if (bomStructures == null || bomStructures.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<Long, ProductionBomStructure> structureById = bomStructures.stream()
                .filter(item -> item != null && item.getId() != null)
                .collect(Collectors.toMap(ProductionBomStructure::getId, item -> item, (left, right) -> left));
        Map<String, BigDecimal> demandedQuantityMap = new HashMap<>();
        Set<String> mergedOutputNodeKeySet = new HashSet<>();
        for (ProductionBomStructure bomStructure : bomStructures) {
            if (bomStructure == null || bomStructure.getTechnologyOperationId() == null) {
                continue;
            }
            // The BOM row points to the producing operation; task quantity should come from that operation's output node.
            ProductionBomStructure outputNode = resolveOperationOutputNode(bomStructure, structureById);
            Long outputProductModelId = resolveOutputProductModelId(outputNode, rootProductModelId);
            if (outputProductModelId == null) {
                continue;
            }
            String mergedOutputNodeKey = buildOperationOutputNodeKey(
                    bomStructure.getTechnologyOperationId(),
                    outputNode == null ? null : outputNode.getId(),
                    outputProductModelId);
            if (!mergedOutputNodeKeySet.add(mergedOutputNodeKey)) {
                continue;
            }
            // demandedQuantity is already the order-level required output quantity for the current output node.
            BigDecimal demandedQuantity = defaultDecimal(outputNode == null ? null : outputNode.getDemandedQuantity());
            String key = buildOperationDemandedQuantityKey(bomStructure.getTechnologyOperationId(), outputProductModelId);
            demandedQuantityMap.merge(key, demandedQuantity, BigDecimal::add);
        }
        return demandedQuantityMap;
    }
    private BigDecimal resolveTaskPlanQuantity(TechnologyRoutingOperation sourceOperation,
                                               Map<String, BigDecimal> operationDemandedQuantityMap,
                                               ProductionOrder productionOrder,
                                               Long rootProductModelId) {
        if (sourceOperation == null || operationDemandedQuantityMap == null || operationDemandedQuantityMap.isEmpty()) {
            return defaultDecimal(productionOrder == null ? null : productionOrder.getQuantity());
        }
        Long outputProductModelId = sourceOperation.getProductModelId() != null
                ? sourceOperation.getProductModelId()
                : rootProductModelId;
        String key = buildOperationDemandedQuantityKey(sourceOperation.getTechnologyOperationId(), outputProductModelId);
        BigDecimal planQuantity = operationDemandedQuantityMap.get(key);
        return planQuantity != null ? planQuantity : defaultDecimal(productionOrder == null ? null : productionOrder.getQuantity());
    }
    private String buildOperationDemandedQuantityKey(Long operationId, Long outputProductModelId) {
        return String.valueOf(operationId) + "#" + String.valueOf(outputProductModelId);
    }
    private String buildOperationOutputNodeKey(Long operationId, Long outputNodeId, Long outputProductModelId) {
        return String.valueOf(operationId) + "#" + String.valueOf(outputNodeId) + "#" + String.valueOf(outputProductModelId);
    }
    private ProductionBomStructure resolveOperationOutputNode(ProductionBomStructure bomStructure,
                                                              Map<Long, ProductionBomStructure> structureById) {
        if (bomStructure == null) {
            return null;
        }
        // The root node is the first output node; child rows use their direct parent as the current operation output.
        if (bomStructure.getParentId() == null) {
            return bomStructure;
        }
        ProductionBomStructure parent = structureById.get(bomStructure.getParentId());
        return parent != null ? parent : bomStructure;
    }
    private Long resolveOutputProductModelId(ProductionBomStructure outputNode,
                                             Long rootProductModelId) {
        if (outputNode == null) {
            return rootProductModelId;
        }
        return outputNode.getProductModelId() != null ? outputNode.getProductModelId() : rootProductModelId;
    }
    private ProductionOrderBom syncProductionOrderBomSnapshot(ProductionOrder productionOrder, TechnologyRouting technologyRouting) {
        // 同步订单BOM快照结构
        if (technologyRouting.getBomId() == null) {
@@ -442,7 +371,9 @@
        productionOrderBomMapper.insert(orderBom);
        Map<Long, Long> idMap = new HashMap<>();
        BigDecimal lastProcessDemandedQuantity = orderQuantity;
        Map<Long, Boolean> operationPickMap = loadOperationPickMap(structureList.stream()
                .map(TechnologyBomStructure::getOperationId)
                .collect(Collectors.toList()));
        for (TechnologyBomStructure source : structureList) {
            // 子节点 parentId 需要映射成新快照节点 id,才能保留原始 BOM 层级。
            ProductionBomStructure target = new ProductionBomStructure();
@@ -451,14 +382,35 @@
            target.setParentId(source.getParentId() == null ? null : idMap.get(source.getParentId()));
            target.setProductModelId(source.getProductModelId());
            target.setTechnologyOperationId(source.getOperationId());
            target.setIsPick(operationPickMap.getOrDefault(source.getOperationId(), Boolean.FALSE));
            target.setUnitQuantity(source.getUnitQuantity());
            target.setDemandedQuantity(lastProcessDemandedQuantity.multiply(source.getUnitQuantity()));
            target.setDemandedQuantity(defaultDecimal(orderQuantity)
                    .multiply(defaultDecimal(source.getUnitQuantity())));
            target.setUnit(source.getUnit());
            productionBomStructureMapper.insert(target);
            idMap.put(source.getId(), target.getId());
            lastProcessDemandedQuantity = target.getDemandedQuantity();
        }
        return orderBom;
    }
    /**
     * 批量读取工序的“是否领料”标记,避免BOM快照逐行查询工序表。
     */
    private Map<Long, Boolean> loadOperationPickMap(List<Long> technologyOperationIds) {
        if (technologyOperationIds == null || technologyOperationIds.isEmpty()) {
            return Collections.emptyMap();
        }
        Set<Long> operationIdSet = technologyOperationIds.stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (operationIdSet.isEmpty()) {
            return Collections.emptyMap();
        }
        return technologyOperationMapper.selectBatchIds(operationIdSet).stream()
                .filter(item -> item != null && item.getId() != null)
                .collect(Collectors.toMap(TechnologyOperation::getId,
                        item -> Boolean.TRUE.equals(item.getIsPick()),
                        (left, right) -> left));
    }
    private void clearProductionSnapshot(Long productionOrderId) {
@@ -554,6 +506,40 @@
    private BigDecimal defaultDecimal(BigDecimal value) {
        // 将空数量兜底为0,避免空指针异常
        return value == null ? BigDecimal.ZERO : value;
    }
    /**
     * 批次号按前缀日期升序排序(格式如 20260703-ed01-001,日期早的在前)
     */
    private void sortBatchNoList(List<String> batchNoList) {
        if (batchNoList == null || batchNoList.size() < 2) {
            return;
        }
        batchNoList.sort(Comparator
                .comparingLong(this::batchDateKey)
                .thenComparing(batchNo -> batchNo == null ? "" : batchNo));
    }
    /**
     * 提取批次号开头的数字作为排序键(yyyyMMdd),无数字时放到最后
     */
    private long batchDateKey(String batchNo) {
        if (batchNo == null) {
            return Long.MAX_VALUE;
        }
        String trimmed = batchNo.trim();
        int end = 0;
        while (end < trimmed.length() && Character.isDigit(trimmed.charAt(end))) {
            end++;
        }
        if (end == 0) {
            return Long.MAX_VALUE;
        }
        try {
            return Long.parseLong(trimmed.substring(0, end));
        } catch (NumberFormatException e) {
            return Long.MAX_VALUE;
        }
    }
    private void validateAndFillOrder(ProductionOrder productionOrder, ProductionOrder oldOrder) {
@@ -843,9 +829,6 @@
                BigDecimal percentage = task.getCompletionStatus() == null
                        ? BigDecimal.ZERO
                        : task.getCompletionStatus();
                if (percentage.compareTo(new BigDecimal("100")) > 0) {
                    percentage = new BigDecimal("100");
                }
                status.setPercentage(percentage);
                statusMap.computeIfAbsent(task.getProductionOrderId(), key -> new ArrayList<>()).add(status);
            }
@@ -886,8 +869,17 @@
                // 遍历处理数据并组装结果
                : workOrderPage.getRecords().stream()
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(ProductionOperationTaskVo::getId, Comparator.nullsLast(Comparator.naturalOrder())))
                .toList();
        if (!workOrderList.isEmpty()) {
            // 工序进度需要按工艺路线工序顺序(drag_sort 升序)从左到右展示,任务 id 仅作为兜底排序
            Map<Long, Integer> routingOperationDragSortMap = loadRoutingOperationDragSortMap(workOrderList);
            Comparator<ProductionOperationTaskVo> workOrderComparator = Comparator
                    .comparing((ProductionOperationTaskVo task) -> routingOperationDragSortMap.get(task.getProductionOrderRoutingOperationId()),
                            Comparator.nullsLast(Comparator.<Integer>naturalOrder()))
                    .thenComparing(ProductionOperationTaskVo::getId,
                            Comparator.nullsLast(Comparator.<Long>naturalOrder()));
            workOrderList = workOrderList.stream().sorted(workOrderComparator).toList();
        }
        if (workOrderList.isEmpty()) {
            detailVo.setWorkOrderList(Collections.emptyList());
            return detailVo;
@@ -1036,6 +1028,23 @@
        return detailVo;
    }
    /**
     * 查询工单对应工艺路线工序的拖动排序,用于按工序先后顺序展示工序进度
     */
    private Map<Long, Integer> loadRoutingOperationDragSortMap(List<ProductionOperationTaskVo> workOrderList) {
        Set<Long> routingOperationIds = workOrderList.stream()
                .map(ProductionOperationTaskVo::getProductionOrderRoutingOperationId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (routingOperationIds.isEmpty()) {
            return Collections.emptyMap();
        }
        return productionOrderRoutingOperationMapper.selectBatchIds(routingOperationIds).stream()
                .filter(Objects::nonNull)
                .filter(operation -> operation.getId() != null && operation.getDragSort() != null)
                .collect(Collectors.toMap(ProductionOrderRoutingOperation::getId, ProductionOrderRoutingOperation::getDragSort, (a, b) -> a));
    }
    private Long resolveProductionOrderId(ProductionOrderDto dto) {
        // 从入参中解析生产订单ID并校验
        if (dto == null) {
@@ -1074,7 +1083,7 @@
            return Collections.emptyList();
        }
        // 查询完整的BOM结构(包括根节点),用于计算层级需求数量
        // 查询完整的BOM结构(包括根节点),每个节点独立计算需求数量
        List<ProductionBomStructureVo> bomStructureList = productionBomStructureMapper.listByBomId(orderBom.getId());
        if (bomStructureList == null || bomStructureList.isEmpty()) {
            return Collections.emptyList();
@@ -1084,33 +1093,68 @@
        ProductionOrder productionOrder = productionOrderMapper.selectById(productionOrderId);
        BigDecimal orderQuantity = productionOrder != null ? defaultDecimal(productionOrder.getQuantity()) : BigDecimal.ZERO;
        // 构建树形结构并计算层级需求数量
        Map<Long, ProductionBomStructureVo> structureByIdMap = bomStructureList.stream()
                .filter(s -> s != null && s.getId() != null)
                .collect(Collectors.toMap(ProductionBomStructureVo::getId, s -> s));
        // 按层级计算需求数量:子级需求数量 = 父级需求数量 × 子级单位产出所需数量
        for (ProductionBomStructureVo structure : bomStructureList) {
            if (structure == null) continue;
            if (structure.getParentId() == null || structure.getParentId() == 0) {
                // 根节点:需求数量 = 订单数量
                structure.setDemandedQuantity(orderQuantity);
            } else {
                // 子节点:需求数量 = 父级需求数量 × 子级单位产出所需数量
                ProductionBomStructureVo parent = structureByIdMap.get(structure.getParentId());
                if (parent != null) {
                    BigDecimal parentDemandedQty = defaultDecimal(parent.getDemandedQuantity());
                    BigDecimal unitQuantity = defaultDecimal(structure.getUnitQuantity());
                    structure.setDemandedQuantity(parentDemandedQty.multiply(unitQuantity));
                }
            if (structure != null) {
                structure.setDemandedQuantity(orderQuantity
                        .multiply(defaultDecimal(structure.getUnitQuantity())));
            }
        }
        // 过滤出非根节点(实际领料项)
        List<ProductionBomStructureVo> childStructureList = bomStructureList.stream()
        // production_bom_structure.is_pick 是生产计划下发时的工序领料标记快照;
        // 领料台账读取时以工序主数据的最新标记为准,保证工序上的“是否领料”调整后无需重新下发即可生效;
        // 工序已被删除时保留快照值,避免历史订单的领料范围丢失。
        Map<Long, Boolean> operationPickMap = loadOperationPickMap(bomStructureList.stream()
                .filter(Objects::nonNull)
                .map(ProductionBomStructureVo::getTechnologyOperationId)
                .collect(Collectors.toList()));
        for (ProductionBomStructureVo structure : bomStructureList) {
            if (structure == null || structure.getTechnologyOperationId() == null) {
                continue;
            }
            Boolean latestPick = operationPickMap.get(structure.getTechnologyOperationId());
            if (latestPick != null) {
                structure.setIsPick(latestPick);
            }
        }
        // 收集订单成品规格ID:生产订单成品规格 + 订单BOM快照的根节点规格,用于排除“自己”这一物料。
        // 注意:BOM结构里领料物料本身也可能是 parent_id 为空的独立节点,这里不能再把所有根节点都当成成品排除,
        // 否则“领料”工序下的物料会被整体过滤掉。
        Set<Long> finishedProductModelIds = new HashSet<>();
        if (productionOrder != null && productionOrder.getProductModelId() != null) {
            finishedProductModelIds.add(productionOrder.getProductModelId());
        }
        if (orderBom.getProductModelId() != null) {
            finishedProductModelIds.add(orderBom.getProductModelId());
        }
        // 收集所有被引用为父级的节点ID:BOM从最后一道工序往里配置前一道工序,
        // 中间节点是上一道工序的产出(自制),旧口径只有每条分支最里层的叶子节点才是真正需要领用的物料。
        Set<Long> parentNodeIds = bomStructureList.stream()
                .filter(s -> s != null && s.getParentId() != null && s.getParentId() != 0)
                .map(ProductionBomStructureVo::getParentId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        // 领料范围以工序的“是否领料”标记为准:只有标记了领料的工序,其消耗的物料才进入领料台账。
        // 注意:领料物料在BOM结构中可能是独立节点(parent_id 为空),所以这里不限制层级,只看工序标记;
        // 只排除与订单成品同规格的物料(成品本身无需领料)。
        List<ProductionBomStructureVo> markedPickStructureList = bomStructureList.stream()
                .filter(s -> s != null && Boolean.TRUE.equals(s.getIsPick()))
                .filter(s -> s.getProductModelId() == null || !finishedProductModelIds.contains(s.getProductModelId()))
                .collect(Collectors.toList());
        // 实际领料项:优先使用工序领料标记;
        // 兼容历史数据:当该订单BOM结构里没有任何工序标记领料时,回退为原来的“叶子节点领料”规则,
        // 避免尚未启用“是否领料”标记的客户打开领料台账时数据突然为空。
        List<ProductionBomStructureVo> childStructureList = markedPickStructureList;
        if (childStructureList.isEmpty()) {
            childStructureList = bomStructureList.stream()
                    .filter(s -> s != null && s.getParentId() != null && s.getParentId() != 0)
                    .filter(s -> s.getId() == null || !parentNodeIds.contains(s.getId()))
                    .filter(s -> s.getProductModelId() == null || !finishedProductModelIds.contains(s.getProductModelId()))
                    .collect(Collectors.toList());
        }
        // 遍历处理数据并组装结果
        List<Long> productModelIds = childStructureList.stream()
@@ -1158,6 +1202,7 @@
                List<String> batchNoList = stockBatchNoMap.get(productModelId) == null
                        ? Collections.emptyList()
                        : new ArrayList<>(stockBatchNoMap.get(productModelId));
                sortBatchNoList(batchNoList);
                vo.setBatchNoList(batchNoList);
                vo.setStockQuantity(stockQuantityMap.getOrDefault(productModelId, BigDecimal.ZERO));
                vo.setBom(true);