src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -22,6 +22,8 @@
import com.ruoyi.production.bean.vo.ProductionOrderVo;
import com.ruoyi.production.bean.vo.ProductionPlanVo;
import com.ruoyi.production.bean.vo.ProductionOrderWorkOrderDetailVo;
import com.ruoyi.production.bean.vo.ProcessRouteStatusVo;
import com.ruoyi.production.bean.vo.ProductionOrderProcessTaskVo;
import com.ruoyi.production.enums.ProductOrderStatusEnum;
import com.ruoyi.production.mapper.*;
import com.ruoyi.production.pojo.*;
@@ -32,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;
@@ -59,6 +62,7 @@
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionOrderBomMapper productionOrderBomMapper;
    private final ProductionBomStructureMapper productionBomStructureMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionProductMainMapper productionProductMainMapper;
    private final ProductionProductOutputMapper productionProductOutputMapper;
    private final ProductionOrderPickMapper productionOrderPickMapper;
@@ -85,6 +89,7 @@
        // 分页查询生产订单
        Page<ProductionOrderVo> result = (Page<ProductionOrderVo>) baseMapper.pageProductionOrder(page, dto);
        fillProductImages(result.getRecords());
        fillProcessRouteStatus(result.getRecords());
        return result;
    }
@@ -93,6 +98,7 @@
        // 查询生产订单列表
        List<ProductionOrderVo> records = baseMapper.listProductionOrder(dto);
        fillProductImages(records);
        fillProcessRouteStatus(records);
        return records;
    }
@@ -132,7 +138,7 @@
                || !Objects.equals(oldOrder.getProductModelId(), productionOrder.getProductModelId())
                || compareDecimal(oldOrder.getQuantity(), productionOrder.getQuantity()) != 0
                || productionOrderRoutingMapper.selectCount(Wrappers.<ProductionOrderRouting>lambdaQuery()
                        .eq(ProductionOrderRouting::getProductionOrderId, productionOrder.getId())) == 0);
                .eq(ProductionOrderRouting::getProductionOrderId, productionOrder.getId())) == 0);
        if (needSync) {
            syncProductionOrderSnapshot(productionOrder.getId());
        }
@@ -188,7 +194,6 @@
            ProductionOrder update = new ProductionOrder();
            update.setId(productionOrder.getId());
            update.setTechnologyRoutingId(targetRoutingId);
        // 持久化或输出处理结果
            if (!this.updateById(update)) {
                throw new ServiceException("绑定工艺路线失败");
            }
@@ -228,6 +233,7 @@
        clearProductionSnapshot(productionOrderId);
        ProductionOrderBom orderBom = syncProductionOrderBomSnapshot(productionOrder, technologyRouting);
        //生产订单工艺路线表
        ProductionOrderRouting orderRouting = new ProductionOrderRouting();
        orderRouting.setProductionOrderId(productionOrder.getId());
        orderRouting.setTechnologyRoutingId(technologyRouting.getId());
@@ -236,7 +242,6 @@
        orderRouting.setDescription(technologyRouting.getDescription());
        orderRouting.setBomId(technologyRouting.getBomId());
        orderRouting.setOrderBomId(orderBom == null ? null : orderBom.getId());
        // 持久化或输出处理结果
        productionOrderRoutingMapper.insert(orderRouting);
        int syncedParamCount = 0;
@@ -246,8 +251,10 @@
                        .eq(TechnologyRoutingOperation::getTechnologyRoutingId, technologyRouting.getId())
                        .orderByDesc(TechnologyRoutingOperation::getDragSort)
                        .orderByDesc(TechnologyRoutingOperation::getId));
        // 扁平 BOM 不配置数量,工单计划数量统一取订单数量。
        BigDecimal taskPlanQuantity = TaskPlanQuantityUtil.resolveTaskPlanQuantity(productionOrder);
        Map<Long, String> operationNameMap = technologyOperationMapper.selectBatchIds(
        // 遍历处理数据并组装结果
                        // 遍历处理数据并组装结果
                        routingOperations.stream()
                                .map(TechnologyRoutingOperation::getTechnologyOperationId)
                                .filter(Objects::nonNull)
@@ -271,6 +278,7 @@
            targetOperation.setIsQuality(sourceOperation.getIsQuality());
            targetOperation.setOperationName(operationNameMap.get(sourceOperation.getTechnologyOperationId()));
            targetOperation.setTechnologyOperationId(sourceOperation.getTechnologyOperationId());
            targetOperation.setType(sourceOperation.getType());
            productionOrderRoutingOperationMapper.insert(targetOperation);
            boolean isLastOperation = lastDragSort != null && Objects.equals(sourceOperation.getDragSort(), lastDragSort);
@@ -278,7 +286,7 @@
                ProductionOperationTask task = new ProductionOperationTask();
                task.setProductionOrderRoutingOperationId(targetOperation.getId());
                task.setProductionOrderId(productionOrder.getId());
                task.setPlanQuantity(defaultDecimal(productionOrder.getQuantity()));
                task.setPlanQuantity(taskPlanQuantity);
                task.setCompleteQuantity(BigDecimal.ZERO);
                task.setWorkOrderNo(generateNextTaskNo());
                task.setStatus(2);
@@ -313,8 +321,11 @@
        return syncedParamCount;
    }
    /**
     * 同步订单BOM快照:扁平复制“物料规格 + 消耗工序”关系。
     * 不再生成成品根节点,也不再计算单位用量和需求数量。
     */
    private ProductionOrderBom syncProductionOrderBomSnapshot(ProductionOrder productionOrder, TechnologyRouting technologyRouting) {
        // 同步订单BOM快照结构
        if (technologyRouting.getBomId() == null) {
            return null;
        }
@@ -322,39 +333,32 @@
        if (technologyBom == null) {
            throw new ServiceException("工艺BOM不存在");
        }
        // 查询并准备业务数据
        // 只取真实物料行:必须有消耗工序,历史成品根节点(无工序)不属于物料
        List<TechnologyBomStructure> structureList = technologyBomStructureMapper.selectList(
                Wrappers.<TechnologyBomStructure>lambdaQuery()
                        .eq(TechnologyBomStructure::getBomId, technologyBom.getId())
                        .isNotNull(TechnologyBomStructure::getOperationId)
                        .orderByAsc(TechnologyBomStructure::getId));
        // 遍历处理数据并组装结果
        TechnologyBomStructure root = structureList.stream().filter(item -> item.getParentId() == null).findFirst().orElse(null);
        BigDecimal orderQuantity = defaultDecimal(productionOrder.getQuantity());
        ProductionOrderBom orderBom = new ProductionOrderBom();
        orderBom.setProductionOrderId(productionOrder.getId());
        orderBom.setBomId(Long.valueOf(technologyBom.getId()));
        orderBom.setProductModelId(root != null ? root.getProductModelId() : productionOrder.getProductModelId());
        // BOM 主表本身即代表成品规格
        orderBom.setProductModelId(technologyBom.getProductModelId() != null
                ? technologyBom.getProductModelId()
                : productionOrder.getProductModelId());
        orderBom.setRemark(technologyBom.getRemark());
        orderBom.setBomNo(technologyBom.getBomNo());
        orderBom.setVersion(technologyBom.getVersion());
        // 持久化或输出处理结果
        productionOrderBomMapper.insert(orderBom);
        Map<Long, Long> idMap = new HashMap<>();
        for (TechnologyBomStructure source : structureList) {
            // 子节点 parentId 需要映射成新快照节点 id,才能保留原始 BOM 层级。
            ProductionBomStructure target = new ProductionBomStructure();
            target.setProductionOrderId(productionOrder.getId());
            target.setProductionOrderBomId(orderBom.getId());
            target.setParentId(source.getParentId() == null ? null : idMap.get(source.getParentId()));
            target.setProductModelId(source.getProductModelId());
            target.setTechnologyOperationId(source.getOperationId());
            target.setUnitQuantity(source.getUnitQuantity());
            target.setDemandedQuantity(source.getUnitQuantity().multiply(orderQuantity));
            target.setUnit(source.getUnit());
            productionBomStructureMapper.insert(target);
            idMap.put(source.getId(), target.getId());
        }
        return orderBom;
    }
@@ -362,7 +366,7 @@
    private void clearProductionSnapshot(Long productionOrderId) {
        // 清理订单已生成的工艺与BOM快照数据
        boolean hasPickRecord = productionOrderPickRecordMapper.selectCount(
        // 查询并准备业务数据
                // 查询并准备业务数据
                Wrappers.<ProductionOrderPickRecord>lambdaQuery()
                        .eq(ProductionOrderPickRecord::getProductionOrderId, productionOrderId)) > 0;
        // 参数与前置条件校验
@@ -372,7 +376,7 @@
        List<Long> taskIds = productionOperationTaskMapper.selectList(
                        Wrappers.<ProductionOperationTask>lambdaQuery()
                                .eq(ProductionOperationTask::getProductionOrderId, productionOrderId))
        // 遍历处理数据并组装结果
                // 遍历处理数据并组装结果
                .stream().map(ProductionOperationTask::getId).collect(Collectors.toList());
        if (!taskIds.isEmpty()) {
            // 已有报工记录说明订单已开工,此时不允许再重建快照。
@@ -714,6 +718,46 @@
        }
    }
    private void fillProcessRouteStatus(List<ProductionOrderVo> records) {
        if (records == null || records.isEmpty()) {
            return;
        }
        List<Long> orderIds = records.stream()
                .map(ProductionOrderVo::getId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (orderIds.isEmpty()) {
            return;
        }
        List<ProductionOrderProcessTaskVo> tasks = productionOperationTaskMapper.listProcessStatusByOrderIds(orderIds);
        Map<Long, List<ProcessRouteStatusVo>> statusMap = new LinkedHashMap<>();
        if (tasks != null) {
            for (ProductionOrderProcessTaskVo task : tasks) {
                if (task == null || task.getProductionOrderId() == null) {
                    continue;
                }
                ProcessRouteStatusVo status = new ProcessRouteStatusVo();
                status.setName(task.getOperationName() != null && !task.getOperationName().isBlank()
                        ? task.getOperationName()
                        : "未知工序");
                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);
            }
        }
        for (ProductionOrderVo record : records) {
            record.setProcessRouteStatus(statusMap.getOrDefault(record.getId(), Collections.emptyList()));
        }
    }
    private StorageBlobVO toStorageBlobVO(StorageBlob blob) {
        // 将存储文件对象转换为VO
        StorageBlobVO vo = BeanUtil.copyProperties(blob, StorageBlobVO.class);
@@ -741,12 +785,12 @@
                new Page<ProductionOperationTaskVo>(1, -1), taskQuery);
        List<ProductionOperationTaskVo> workOrderList = workOrderPage == null || workOrderPage.getRecords() == null
                ? Collections.emptyList()
        // 遍历处理数据并组装结果
                // 遍历处理数据并组装结果
                : workOrderPage.getRecords().stream()
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(ProductionOperationTaskVo::getId, Comparator.nullsLast(Comparator.naturalOrder())))
                .collect(Collectors.toList());
        if (workOrderList == null || workOrderList.isEmpty()) {
                .toList();
        if (workOrderList.isEmpty()) {
            detailVo.setWorkOrderList(Collections.emptyList());
            return detailVo;
        }
@@ -757,7 +801,7 @@
                .collect(Collectors.toList());
        List<ProductionProductMain> reportMainList = workOrderIdList.isEmpty()
                ? Collections.emptyList()
        // 查询并准备业务数据
                // 查询并准备业务数据
                : productionProductMainMapper.selectList(
                Wrappers.<ProductionProductMain>lambdaQuery()
                        .in(ProductionProductMain::getProductionOperationTaskId, workOrderIdList)
@@ -788,12 +832,10 @@
                if (reportOutput == null) {
                    continue;
                }
                Long reportMainId = reportOutput.getProductionProductMainId() != null
                        ? reportOutput.getProductionProductMainId()
                        : reportOutput.getProductMainId();
                if (reportMainId == null) {
                if (reportOutput.getProductionProductMainId() == null) {
                    continue;
                }
                Long reportMainId = reportOutput.getProductionProductMainId();
                reportOutputMap.computeIfAbsent(reportMainId, k -> new ArrayList<>()).add(reportOutput);
            }
@@ -868,6 +910,7 @@
                ProductionOrderWorkOrderDetailVo.ReportDetail reportDetail = new ProductionOrderWorkOrderDetailVo.ReportDetail();
                reportDetail.setReportMain(reportMain);
                reportDetail.setWorkHour(reportMain.getWorkHour());
                reportDetail.setReportOutputList(reportOutputMap.getOrDefault(reportMainId, Collections.emptyList()));
                reportDetail.setReportParamList(reportParamMap.getOrDefault(reportMainId, Collections.emptyList()));
                reportDetailList.add(reportDetail);
@@ -878,6 +921,7 @@
                    inspectDetail.setReportId(reportMainId);
                    inspectDetail.setReportNo(reportMain.getProductNo());
                    inspectDetail.setReportMain(reportMain);
                    inspectDetail.setWorkHour(reportMain.getWorkHour());
                    inspectDetail.setInspect(inspect);
                    inspectDetail.setInspectParamList(inspectParamMap.getOrDefault(inspect.getId(), Collections.emptyList()));
                    inspectDetail.setInspectFileList(inspectFileMap.getOrDefault(inspect.getId(), Collections.emptyList()));
@@ -917,12 +961,20 @@
    @Override
    public List<ProductionOrderPickVo> pick(Long productionOrderId) {
        // 查询订单领料、投料与退料明细
        return pick(productionOrderId, null);
    }
    /**
     * 按订单BOM快照给出可领用物料建议。
     * 扁平BOM只描述“物料规格 + 消耗工序”,因此这里不再计算需求数量;
     * 传入工序ID时只返回该工序的物料。
     */
    @Override
    public List<ProductionOrderPickVo> pick(Long productionOrderId, Long technologyOperationId) {
        if (productionOrderId == null) {
            return Collections.emptyList();
        }
        // 查询并准备业务数据
        ProductionOrderBom orderBom = productionOrderBomMapper.selectOne(
                Wrappers.<ProductionOrderBom>lambdaQuery()
                        .eq(ProductionOrderBom::getProductionOrderId, productionOrderId)
@@ -932,13 +984,35 @@
            return Collections.emptyList();
        }
        List<ProductionBomStructureVo> bomStructureList = productionBomStructureMapper.pickByBomId(orderBom.getId());
        List<ProductionBomStructureVo> bomStructureList = productionBomStructureMapper.listByBomId(orderBom.getId());
        if (bomStructureList == null || bomStructureList.isEmpty()) {
            return Collections.emptyList();
        }
        ProductionOrder productionOrder = productionOrderMapper.selectById(productionOrderId);
        // 成品规格不需要领料:兼容历史快照中可能存在的成品根节点
        Set<Long> finishedProductModelIds = new HashSet<>();
        if (productionOrder != null && productionOrder.getProductModelId() != null) {
            finishedProductModelIds.add(productionOrder.getProductModelId());
        }
        if (orderBom.getProductModelId() != null) {
            finishedProductModelIds.add(orderBom.getProductModelId());
        }
        List<ProductionBomStructureVo> materialStructureList = bomStructureList.stream()
                .filter(Objects::nonNull)
                .filter(s -> s.getProductModelId() != null && !finishedProductModelIds.contains(s.getProductModelId()))
                .filter(s -> s.getTechnologyOperationId() != null)
                .filter(s -> technologyOperationId == null
                        || technologyOperationId.equals(s.getTechnologyOperationId()))
                .collect(Collectors.toList());
        if (materialStructureList.isEmpty()) {
            return Collections.emptyList();
        }
        // 遍历处理数据并组装结果
        List<Long> productModelIds = bomStructureList.stream()
        List<Long> productModelIds = materialStructureList.stream()
                .map(ProductionBomStructureVo::getProductModelId)
                .filter(Objects::nonNull)
                .distinct()
@@ -948,7 +1022,8 @@
        if (!productModelIds.isEmpty()) {
            List<StockInventory> stockList = stockInventoryMapper.selectList(
                    Wrappers.<StockInventory>lambdaQuery()
                            .in(StockInventory::getProductModelId, productModelIds));
                            .in(StockInventory::getProductModelId, productModelIds)
                            .gt(StockInventory::getQualitity, BigDecimal.ZERO));
            for (StockInventory stockItem : stockList) {
                if (stockItem == null || stockItem.getProductModelId() == null) {
                    continue;
@@ -963,31 +1038,26 @@
        }
        Map<String, ProductionOrderPickVo> mergedPickMap = new LinkedHashMap<>();
        for (ProductionBomStructureVo structure : bomStructureList) {
            if (structure == null || structure.getProductModelId() == null) {
        for (ProductionBomStructureVo structure : materialStructureList) {
            Long productModelId = structure.getProductModelId();
            String mergeKey = structure.getTechnologyOperationId() + "#" + productModelId;
            if (mergedPickMap.containsKey(mergeKey)) {
                continue;
            }
            Long productModelId = structure.getProductModelId();
            String mergeKey = String.valueOf(structure.getTechnologyOperationId()) + "#" + productModelId;
            ProductionOrderPickVo vo = mergedPickMap.get(mergeKey);
            if (vo == null) {
                vo = new ProductionOrderPickVo();
                vo.setProductModelId(productModelId);
                vo.setOperationName(structure.getOperationName());
                vo.setTechnologyOperationId(structure.getTechnologyOperationId());
                vo.setProductName(structure.getProductName());
                vo.setModel(structure.getModel());
                vo.setDemandedQuantity(BigDecimal.ZERO);
                vo.setUnit(structure.getUnit());
                List<String> batchNoList = stockBatchNoMap.get(productModelId) == null
                        ? Collections.emptyList()
                        : new ArrayList<>(stockBatchNoMap.get(productModelId));
                vo.setBatchNoList(batchNoList);
                vo.setStockQuantity(stockQuantityMap.getOrDefault(productModelId, BigDecimal.ZERO));
                vo.setBom(true);
                mergedPickMap.put(mergeKey, vo);
            }
            vo.setDemandedQuantity(defaultDecimal(vo.getDemandedQuantity()).add(defaultDecimal(structure.getDemandedQuantity())));
            ProductionOrderPickVo vo = new ProductionOrderPickVo();
            vo.setProductModelId(productModelId);
            vo.setOperationName(structure.getOperationName());
            vo.setTechnologyOperationId(structure.getTechnologyOperationId());
            vo.setProductName(structure.getProductName());
            vo.setModel(structure.getModel());
            vo.setUnit(structure.getUnit());
            List<String> batchNoList = stockBatchNoMap.get(productModelId) == null
                    ? Collections.emptyList()
                    : new ArrayList<>(stockBatchNoMap.get(productModelId));
            vo.setBatchNoList(batchNoList);
            vo.setStockQuantity(stockQuantityMap.getOrDefault(productModelId, BigDecimal.ZERO));
            vo.setBom(true);
            mergedPickMap.put(mergeKey, vo);
        }
        return new ArrayList<>(mergedPickMap.values());
    }