5 天以前 4bca60653b3f14448c496618cc4cef5862cfe898
Merge remote-tracking branch 'origin/dev_New_pro' into dev_New_pro
已修改4个文件
263 ■■■■ 文件已修改
src/main/java/com/ruoyi/common/enums/StockInQualifiedRecordTypeEnum.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/StockOutQualifiedRecordTypeEnum.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java 243 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/StockInQualifiedRecordTypeEnum.java
@@ -23,8 +23,9 @@
    SALE_SHIP_STOCK_OUT("13", "销售-发货出库"),
    RETURN_HE_IN("14", "销售退货-合格入库"),
    RETURN_UNSTOCK_IN("15", "销售退货-不合格入库"),
    PICK_RETURN_IN("20", "销售退货-合格入库"),
    PURCHASE_RETURN_STOCK_OUT("21", "采购退货");
    PICK_RETURN_IN("20", "领料退料-合格入库"),
    PURCHASE_RETURN_STOCK_OUT("21", "采购退货"),
    FEED_RETURN_IN("22", "生产退料-合格入库");
src/main/java/com/ruoyi/common/enums/StockOutQualifiedRecordTypeEnum.java
@@ -9,7 +9,9 @@
    PRODUCTION_REPORT_STOCK_OUT("3", "生产报工-出库"),
    SALE_STOCK_OUT("8", "销售-出库"),
    PURCHASE_RETURN_STOCK_OUT("9", "采购退货"),
    SALE_SHIP_STOCK_OUT("13", "销售-发货出库");
    SALE_SHIP_STOCK_OUT("13", "销售-发货出库"),
    PICK_STOCK_OUT("14", "生产领料出库"),
    FEED_STOCK_OUT("15", "生产补料出库");
    private final String code;
    private final String value;
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java
@@ -3,7 +3,9 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.enums.ReviewStatusEnum;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.production.bean.dto.ProductionOrderPickDto;
@@ -18,8 +20,12 @@
import com.ruoyi.production.service.ProductionOrderPickService;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.pojo.StockInRecord;
import com.ruoyi.stock.pojo.StockInventory;
import com.ruoyi.stock.pojo.StockOutRecord;
import com.ruoyi.stock.service.StockInventoryService;
import com.ruoyi.stock.service.StockInRecordService;
import com.ruoyi.stock.service.StockOutRecordService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -39,12 +45,18 @@
    private static final byte PICK_TYPE_NORMAL = 1;
    private static final byte PICK_TYPE_FEEDING = 2;
    private static final String PICK_STOCK_OUT_RECORD_TYPE = StockOutQualifiedRecordTypeEnum.PICK_STOCK_OUT.getCode();
    private static final String FEED_STOCK_OUT_RECORD_TYPE = StockOutQualifiedRecordTypeEnum.FEED_STOCK_OUT.getCode();
    private static final String PICK_RETURN_IN_RECORD_TYPE = StockInQualifiedRecordTypeEnum.PICK_RETURN_IN.getCode();
    private static final String FEED_RETURN_IN_RECORD_TYPE = StockInQualifiedRecordTypeEnum.FEED_RETURN_IN.getCode();
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionOrderPickRecordMapper productionOrderPickRecordMapper;
    private final StockInventoryMapper stockInventoryMapper;
    private final StockInventoryService stockInventoryService;
    private final StockInRecordService stockInRecordService;
    private final StockOutRecordService stockOutRecordService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -52,8 +64,9 @@
        // 领料新增总流程:
        // 1) 解析前端行数据并逐行合并参数;
        // 2) 校验参数与批次;
        // 3) 先扣减库存,再落库领料主记录;
        // 4) 写入领料流水,记录数量变化轨迹。
        // 3) 先保存领料主记录;
        // 4) 再走“出库申请 + 审批通过”完成库存扣减;
        // 5) 写入领料流水,记录数量变化轨迹。
        List<ProductionOrderPickDto> pickItems = resolvePickItems(dto);
        // 逐行处理领料数据,行号用于拼装精确的报错信息。
        for (int i = 0; i < pickItems.size(); i++) {
@@ -66,7 +79,6 @@
            List<String> batchNoList = resolveBatchNoList(resolvedDto);
            String inventoryBatchNo = pickInventoryBatchNo(batchNoList);
            String storedBatchNo = formatBatchNoStorage(batchNoList);
            subtractInventory(resolvedDto.getProductModelId(), storedBatchNo, resolvedDto.getPickQuantity(), rowNo);
            // 保存领料主记录快照。
            ProductionOrderPick orderPick = new ProductionOrderPick();
@@ -82,6 +94,9 @@
            orderPick.setReturned(false);
            // 新增主记录。
            baseMapper.insert(orderPick);
            // 先新增出库申请,再审批通过,完成库存扣减。
            subtractInventory(orderPick.getId(), resolvedDto.getProductModelId(), storedBatchNo, resolvedDto.getPickQuantity(), rowNo, PICK_STOCK_OUT_RECORD_TYPE);
            // 记录本次领料流水(before=0,after=本次领料量)。
            insertPickRecord(orderPick.getId(),
@@ -201,7 +216,12 @@
            }
            String oldBatchNo = resolveInventoryBatchNoFromStored(existingPick.getBatchNo());
            BigDecimal oldQuantity = defaultDecimal(existingPick.getQuantity());
            addInventory(existingPick.getProductModelId(), oldBatchNo, oldQuantity);
            addInventory(existingPick.getId(), existingPick.getProductModelId(), oldBatchNo, oldQuantity, PICK_RETURN_IN_RECORD_TYPE);
            // 删除关联领料流水,避免遗留无主记录。
            productionOrderPickRecordMapper.delete(
                    Wrappers.<ProductionOrderPickRecord>lambdaQuery()
                            .eq(ProductionOrderPickRecord::getPickId, existingPick.getId())
            );
            int affected = baseMapper.deleteById(deleteId);
            if (affected <= 0) {
                throw new ServiceException("删除领料记录失败,ID=" + deleteId);
@@ -240,7 +260,12 @@
        for (ProductionOrderPick missingPick : missingPickList) {
            String oldBatchNo = resolveInventoryBatchNoFromStored(missingPick.getBatchNo());
            BigDecimal oldQuantity = defaultDecimal(missingPick.getQuantity());
            addInventory(missingPick.getProductModelId(), oldBatchNo, oldQuantity);
            addInventory(missingPick.getId(), missingPick.getProductModelId(), oldBatchNo, oldQuantity, PICK_RETURN_IN_RECORD_TYPE);
            // 删除关联领料流水,避免遗留无主记录。
            productionOrderPickRecordMapper.delete(
                    Wrappers.<ProductionOrderPickRecord>lambdaQuery()
                            .eq(ProductionOrderPickRecord::getPickId, missingPick.getId())
            );
            int affected = baseMapper.deleteById(missingPick.getId());
            if (affected <= 0) {
                throw new ServiceException("删除未回传领料记录失败,ID=" + missingPick.getId());
@@ -261,11 +286,11 @@
    }
    private void addNewPickInUpdate(ProductionOrderPickDto dto, int rowNo) {
        // 更新场景下新增一条领料:扣库存 -> 新增主记录 -> 写流水。
        // 更新场景下新增一条领料:
        // 新增主记录 -> 出库申请并审批 -> 写流水。
        List<String> batchNoList = resolveBatchNoList(dto);
        String inventoryBatchNo = pickInventoryBatchNo(batchNoList);
        String storedBatchNo = formatBatchNoStorage(batchNoList);
        subtractInventory(dto.getProductModelId(), storedBatchNo, dto.getPickQuantity(), rowNo);
        ProductionOrderPick orderPick = new ProductionOrderPick();
        orderPick.setProductionOrderId(dto.getProductionOrderId());
@@ -279,6 +304,9 @@
        orderPick.setBom(dto.getBom());
        orderPick.setReturned(false);
        baseMapper.insert(orderPick);
        // 先新增出库申请,再审批通过,完成库存扣减。
        subtractInventory(orderPick.getId(), dto.getProductModelId(), storedBatchNo, dto.getPickQuantity(), rowNo, PICK_STOCK_OUT_RECORD_TYPE);
        insertPickRecord(orderPick.getId(),
                dto.getProductionOrderId(),
@@ -337,7 +365,7 @@
                : formatBatchNoStorage(batchNoList);
        BigDecimal feedingQuantity = dto.getFeedingQuantity();
        subtractInventory(productModelId, inventoryBatchNo, feedingQuantity, rowNo);
        subtractInventory(oldPick.getId(), productModelId, inventoryBatchNo, feedingQuantity, rowNo, FEED_STOCK_OUT_RECORD_TYPE);
        // 计算补料前后数量并写补料流水。
        BigDecimal beforeFeedingQty = sumFeedingQuantity(dto.getProductionOrderId(), oldPick.getId());
@@ -393,19 +421,36 @@
    }
    private void updateReturnPick(ProductionOrderPickDto dto, ProductionOrderPick oldPick, int rowNo) {
        // 退料更新只改主领料记录中的退料字段与实际量。
        // 退料更新:
        // 1) returnQty 按“本次退料量”处理;
        // 2) 本次退料量回补到“生产退料入库”;
        // 3) 累加主记录退料总量并重算实际量。
        BigDecimal oldReturnQty = defaultDecimal(oldPick.getReturnQty());
        BigDecimal currentReturnQty = defaultDecimal(dto.getReturnQty());
        BigDecimal totalReturnQty = oldReturnQty.add(currentReturnQty);
        if (currentReturnQty.compareTo(BigDecimal.ZERO) > 0) {
            String returnBatchNo = resolveInventoryBatchNoFromStored(oldPick.getBatchNo());
            addInventory(oldPick.getId(), oldPick.getProductModelId(), returnBatchNo, currentReturnQty, FEED_RETURN_IN_RECORD_TYPE);
        }
        BigDecimal actualQty = defaultDecimal(oldPick.getQuantity())
                .add(defaultDecimal(oldPick.getFeedingQty()))
                .subtract(totalReturnQty);
        if (actualQty.compareTo(BigDecimal.ZERO) < 0) {
            throw new ServiceException("第" + rowNo + "行退料失败:累计退料数量不能大于可用数量");
        }
        ProductionOrderPick updatePick = new ProductionOrderPick();
        updatePick.setId(oldPick.getId());
        updatePick.setReturnQty(dto.getReturnQty());
        updatePick.setActualQty(dto.getActualQty());
        updatePick.setReturned(true);
        updatePick.setReturnQty(totalReturnQty);
        updatePick.setActualQty(actualQty);
        updatePick.setReturned(totalReturnQty.compareTo(BigDecimal.ZERO) > 0);
        int affected = baseMapper.updateById(updatePick);
        if (affected <= 0) {
            throw new ServiceException("第" + rowNo + "行退料失败:更新领料主记录失败");
        }
        oldPick.setReturnQty(updatePick.getReturnQty());
        oldPick.setActualQty(updatePick.getActualQty());
        oldPick.setReturned(true);
        oldPick.setReturned(updatePick.getReturned());
    }
    private void updateExistingPick(ProductionOrderPickDto dto,
@@ -431,21 +476,30 @@
        String newStoredBatchNo = formatBatchNoStorage(newBatchNoList);
        BigDecimal newQuantity = dto.getPickQuantity();
        // 判断规格+批次是否变化,决定库存处理策略。
        // 判断规格+批次或数量是否变化,并按场景处理库存:
        // 1) 同规格同批次:按差值处理(增量扣减 / 减量回退);
        // 2) 规格或批次变化:回退旧领料后再重提新领料。
        boolean sameStockKey = Objects.equals(oldProductModelId, newProductModelId)
                && Objects.equals(oldBatchNo, newBatchNo);
        boolean quantityChanged = oldQuantity.compareTo(newQuantity) != 0;
        boolean needReissuePickRecord = !sameStockKey || quantityChanged;
        if (sameStockKey) {
            // 规格与批次不变:只按差值增减库存。
            BigDecimal delta = newQuantity.subtract(oldQuantity);
            if (delta.compareTo(BigDecimal.ZERO) > 0) {
                subtractInventory(newProductModelId, newStoredBatchNo, delta, rowNo);
            } else if (delta.compareTo(BigDecimal.ZERO) < 0) {
                addInventory(oldProductModelId, oldBatchNo, delta.abs());
            BigDecimal deltaQuantity = newQuantity.subtract(oldQuantity);
            if (deltaQuantity.compareTo(BigDecimal.ZERO) > 0) {
                // 数量增加,只扣减新增部分。
                subtractInventory(oldPick.getId(), newProductModelId, newStoredBatchNo, deltaQuantity, rowNo, PICK_STOCK_OUT_RECORD_TYPE);
            } else if (deltaQuantity.compareTo(BigDecimal.ZERO) < 0) {
                // 数量减少,只回退差值部分。
                addInventory(oldPick.getId(), oldProductModelId, oldBatchNo, deltaQuantity.abs(), PICK_RETURN_IN_RECORD_TYPE);
            }
        } else {
            // 规格或批次变化:先回补旧库存,再扣减新库存。
            addInventory(oldProductModelId, oldBatchNo, oldQuantity);
            subtractInventory(newProductModelId, newStoredBatchNo, newQuantity, rowNo);
            // 规格或批次变化:先全量回退旧领料,再全量扣减新领料。
            addInventory(oldPick.getId(), oldProductModelId, oldBatchNo, oldQuantity, PICK_RETURN_IN_RECORD_TYPE);
            subtractInventory(oldPick.getId(), newProductModelId, newStoredBatchNo, newQuantity, rowNo, PICK_STOCK_OUT_RECORD_TYPE);
        }
        if (needReissuePickRecord) {
            // 正常领料流水按“最新领料量”重建,避免保留历史旧值。
            deleteNormalPickRecord(oldPick.getId());
        }
        oldPick.setProductModelId(newProductModelId);
@@ -454,6 +508,9 @@
        oldPick.setRemark(dto.getRemark());
        oldPick.setOperationName(dto.getOperationName());
        oldPick.setTechnologyOperationId(dto.getTechnologyOperationId());
        // 普通更新也要同步重算实际用量,避免沿用旧值。
        // 规则:实际用量 = 领料数量 + 补料数量 - 退料数量。
        oldPick.setActualQty(calculateActualQty(oldPick, oldPick.getFeedingQty()));
        if (dto.getDemandedQuantity() != null) {
            oldPick.setDemandedQuantity(dto.getDemandedQuantity());
        }
@@ -465,16 +522,15 @@
            throw new ServiceException("第" + rowNo + "行更新失败:更新领料记录失败");
        }
        // 写入更新流水,保留本次数量变化轨迹。
        BigDecimal recordQuantity = sameStockKey ? oldQuantity.subtract(newQuantity).abs() : newQuantity;
        if (recordQuantity.compareTo(BigDecimal.ZERO) > 0 || oldQuantity.compareTo(newQuantity) != 0 || !sameStockKey) {
        // 如果发生领料重提,补写一条新的正常领料流水。
        if (needReissuePickRecord) {
            insertPickRecord(oldPick.getId(),
                    dto.getProductionOrderId(),
                    dto.getProductionOperationTaskId(),
                    newProductModelId,
                    newBatchNo,
                    recordQuantity,
                    oldQuantity,
                    newQuantity,
                    BigDecimal.ZERO,
                    newQuantity,
                    dto.getPickType(),
                    dto.getRemark(),
@@ -509,11 +565,28 @@
        productionOrderPickRecordMapper.insert(pickRecord);
    }
    private void subtractInventory(Long productModelId, String batchNo, BigDecimal quantity, int rowNo) {
    private void deleteNormalPickRecord(Long pickId) {
        // 删除该领料单历史上的“正常领料”流水,保留补料/退料流水。
        if (pickId == null) {
            return;
        }
        productionOrderPickRecordMapper.delete(
                Wrappers.<ProductionOrderPickRecord>lambdaQuery()
                        .eq(ProductionOrderPickRecord::getPickId, pickId)
                        .eq(ProductionOrderPickRecord::getPickType, PICK_TYPE_NORMAL)
        );
    }
    private void subtractInventory(Long recordId,
                                   Long productModelId,
                                   String batchNo,
                                   BigDecimal quantity,
                                   int rowNo,
                                   String stockOutRecordType) {
        // 扣减库存总流程:
        // 1) 解析批次列表;
        // 2) 计算每个批次可用量与总可用量;
        // 3) 按批次顺序逐笔扣减,直到扣完目标数量;
        // 3) 按批次顺序逐笔“新增出库记录并审批通过”,直到扣完目标数量;
        // 4) 任一步失败即抛错并回滚事务。
        BigDecimal deductQuantity = defaultDecimal(quantity);
        // 领料数量小于等于0时,不需要执行库存扣减。
@@ -562,14 +635,7 @@
                continue;
            }
            BigDecimal currentDeductQuantity = remainingQuantity.min(availableQuantity);
            StockInventoryDto stockInventoryDto = new StockInventoryDto();
            stockInventoryDto.setProductModelId(productModelId);
            stockInventoryDto.setBatchNo(entry.getKey());
            stockInventoryDto.setQualitity(currentDeductQuantity);
            int affected = stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto);
            if (affected <= 0) {
                throw new ServiceException("第" + rowNo + "行扣减库存失败:库存更新失败");
            }
            createAndApproveStockOutRecord(recordId, productModelId, entry.getKey(), currentDeductQuantity, rowNo, stockOutRecordType);
            remainingQuantity = remainingQuantity.subtract(currentDeductQuantity);
        }
@@ -578,19 +644,97 @@
        }
    }
    private void addInventory(Long productModelId, String batchNo, BigDecimal quantity) {
        // 回补库存(用于删除领料、改小领料、切换批次等场景)。
    private void createAndApproveStockOutRecord(Long recordId,
                                                Long productModelId,
                                                String batchNo,
                                                BigDecimal quantity,
                                                int rowNo,
                                                String stockOutRecordType) {
        // 库存扣减改为两步:
        // 1) 先调用 addStockOutRecordOnly 新增待审批出库记录;
        // 2) 再调用出库审批,审批状态固定传 1(通过)。
        try {
            StockInventoryDto stockInventoryDto = new StockInventoryDto();
            stockInventoryDto.setRecordId(recordId == null ? 0L : recordId);
            stockInventoryDto.setRecordType(stockOutRecordType);
            stockInventoryDto.setProductModelId(productModelId);
            stockInventoryDto.setBatchNo(batchNo);
            stockInventoryDto.setQualitity(quantity);
            stockInventoryService.addStockOutRecordOnly(stockInventoryDto);
            LambdaQueryWrapper<StockOutRecord> recordWrapper = Wrappers.<StockOutRecord>lambdaQuery()
                    .eq(StockOutRecord::getRecordId, stockInventoryDto.getRecordId())
                    .eq(StockOutRecord::getRecordType, stockOutRecordType)
                    .eq(StockOutRecord::getProductModelId, productModelId)
                    .eq(StockOutRecord::getType, "0")
                    .orderByDesc(StockOutRecord::getId)
                    .last("limit 1");
            if (StringUtils.isEmpty(batchNo)) {
                recordWrapper.isNull(StockOutRecord::getBatchNo);
            } else {
                recordWrapper.eq(StockOutRecord::getBatchNo, batchNo);
            }
            StockOutRecord stockOutRecord = stockOutRecordService.getOne(recordWrapper, false);
            if (stockOutRecord == null || stockOutRecord.getId() == null) {
                throw new ServiceException("第" + rowNo + "行扣减库存失败:未找到对应出库申请记录");
            }
            stockOutRecordService.batchApprove(
                    Collections.singletonList(stockOutRecord.getId()),
                    ReviewStatusEnum.APPROVED.getCode()
            );
        } catch (ServiceException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new ServiceException("第" + rowNo + "行扣减库存失败:" + ex.getMessage());
        }
    }
    private void addInventory(Long recordId,
                              Long productModelId,
                              String batchNo,
                              BigDecimal quantity,
                              String stockInRecordType) {
        // 回补库存改为两步:
        // 1) 先新增入库申请;
        // 2) 再审批通过,确保库存立刻回补生效。
        BigDecimal addQuantity = defaultDecimal(quantity);
        if (addQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setProductModelId(productModelId);
        stockInventoryDto.setBatchNo(batchNo);
        stockInventoryDto.setQualitity(addQuantity);
        stockInventoryDto.setRecordType(String.valueOf(StockInQualifiedRecordTypeEnum.PICK_RETURN_IN.getCode()));
        stockInventoryDto.setRecordId(0L);
        stockInventoryService.addStockInRecordOnly(stockInventoryDto);
        try {
            StockInventoryDto stockInventoryDto = new StockInventoryDto();
            stockInventoryDto.setProductModelId(productModelId);
            stockInventoryDto.setBatchNo(batchNo);
            stockInventoryDto.setQualitity(addQuantity);
            stockInventoryDto.setRecordType(stockInRecordType);
            stockInventoryDto.setRecordId(recordId == null ? 0L : recordId);
            stockInventoryService.addStockInRecordOnly(stockInventoryDto);
            LambdaQueryWrapper<StockInRecord> recordWrapper = Wrappers.<StockInRecord>lambdaQuery()
                    .eq(StockInRecord::getRecordId, stockInventoryDto.getRecordId())
                    .eq(StockInRecord::getRecordType, stockInventoryDto.getRecordType())
                    .eq(StockInRecord::getProductModelId, productModelId)
                    .eq(StockInRecord::getType, "0")
                    .orderByDesc(StockInRecord::getId)
                    .last("limit 1");
            if (StringUtils.isEmpty(batchNo)) {
                recordWrapper.isNull(StockInRecord::getBatchNo);
            } else {
                recordWrapper.eq(StockInRecord::getBatchNo, batchNo);
            }
            StockInRecord stockInRecord = stockInRecordService.getOne(recordWrapper, false);
            if (stockInRecord == null || stockInRecord.getId() == null) {
                throw new ServiceException("回补库存失败:未找到对应入库申请记录");
            }
            stockInRecordService.batchApprove(
                    Collections.singletonList(stockInRecord.getId()),
                    ReviewStatusEnum.APPROVED.getCode()
            );
        } catch (ServiceException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new ServiceException("回补库存失败:" + ex.getMessage());
        }
    }
    private List<ProductionOrderPickDto> resolvePickItems(ProductionOrderPickDto dto) {
@@ -769,7 +913,7 @@
    }
    private void validateReturnParam(ProductionOrderPickDto dto, int rowNo) {
        // 校验退料参数(订单、领料ID、退料量、实际量)。
        // 校验退料参数(订单、领料ID、退料量)。
        if (dto.getProductionOrderId() == null) {
            throw new ServiceException("第" + rowNo + "行参数错误:生产订单ID不能为空");
        }
@@ -778,9 +922,6 @@
        }
        if (dto.getReturnQty() == null || dto.getReturnQty().compareTo(BigDecimal.ZERO) < 0) {
            throw new ServiceException("第" + rowNo + "行参数错误:退料数量不能小于0");
        }
        if (dto.getActualQty() == null || dto.getActualQty().compareTo(BigDecimal.ZERO) < 0) {
            throw new ServiceException("第" + rowNo + "行参数错误:实际数量不能小于0");
        }
    }
src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java
@@ -101,12 +101,13 @@
            throw new BaseException("合并失败,所选生产计划的产品型号不一致");
        }
        // 已下发或部分下发的计划不允许再次合并
        boolean hasIssuedPlan = planLists.stream()
        // 仅“已下发”计划不允许再次参与合并下发;
        // “待下发/部分下发”允许继续下发剩余数量。
        boolean hasFullyIssuedPlan = planLists.stream()
                .anyMatch(item -> item.getStatus() != null
                        && (item.getStatus() == PLAN_STATUS_PARTIAL || item.getStatus() == PLAN_STATUS_ISSUED));
        if (hasIssuedPlan) {
            throw new BaseException("合并失败,所选生产计划存在已下发或部分下发的数据");
                        && item.getStatus() == PLAN_STATUS_ISSUED);
        if (hasFullyIssuedPlan) {
            throw new BaseException("合并失败,所选生产计划存在已下发的数据");
        }
        // 计算本次可下发的剩余需求总量