gaoluyang
10 天以前 a829dd34332bbc10c6b42ef86635ed48e34c6207
fix: 原料领料批号按可用库存过滤并实现先进先出

- 批号下拉仅返回仍有可用库存(数量扣除锁定后 > 0)的批次,
已用完的历史批号不再出现在领料下拉框
- 批次按首次入库时间升序排列,历史导入数据以入库记录时间为准
- 领料扣减库存时由后端按批次首次入库时间重新排序,忽略前端勾选顺序

Co-Authored-By: Claude Code <noreply@anthropic.com>
已修改3个文件
98 ■■■■■ 文件已修改
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/ProductModelMapper.xml 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java
@@ -31,6 +31,7 @@
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -599,13 +600,53 @@
            batchNoList = Collections.singletonList(null);
        }
        // 先查询各批次库存记录,并按首次入库时间升序排列(先进先出),
        // 不依赖前端勾选顺序,避免人工多选时扣错批次。
        Map<String, StockInventory> inventoryMap = new LinkedHashMap<>();
        for (String currentBatchNo : batchNoList) {
            inventoryMap.put(currentBatchNo,
                    stockInventoryMapper.selectOne(buildStockWrapper(productModelId, currentBatchNo)));
        }
        // 首次入库时间以入库记录为准:历史数据批量导入时 stock_inventory.create_time 可能相同,
        // 不能准确反映批次先后。
        Map<String, LocalDateTime> firstInTimeMap = new HashMap<>();
        List<String> nonNullBatchNoList = batchNoList.stream()
                .filter(StringUtils::isNotEmpty)
                .collect(Collectors.toList());
        if (!nonNullBatchNoList.isEmpty()) {
            List<StockInRecord> stockInRecords = stockInRecordService.list(
                    Wrappers.<StockInRecord>lambdaQuery()
                            .eq(StockInRecord::getProductModelId, productModelId)
                            .in(StockInRecord::getBatchNo, nonNullBatchNoList));
            for (StockInRecord stockInRecord : stockInRecords) {
                if (stockInRecord == null || stockInRecord.getBatchNo() == null
                        || stockInRecord.getCreateTime() == null) {
                    continue;
                }
                firstInTimeMap.merge(stockInRecord.getBatchNo(), stockInRecord.getCreateTime(),
                        (a, b) -> a.isBefore(b) ? a : b);
            }
        }
        List<String> fifoBatchNoList = inventoryMap.entrySet().stream()
                .sorted(Comparator
                        .comparing((Map.Entry<String, StockInventory> entry) -> firstInTimeMap.get(entry.getKey()),
                                Comparator.nullsLast(Comparator.naturalOrder()))
                        .thenComparing(entry -> entry.getValue() == null ? null : entry.getValue().getCreateTime(),
                                Comparator.nullsLast(Comparator.naturalOrder()))
                        .thenComparing(entry -> {
                            StockInventory inventory = entry.getValue();
                            return inventory == null || inventory.getId() == null
                                    ? Long.MAX_VALUE : inventory.getId();
                        }))
                .map(Map.Entry::getKey)
                .collect(Collectors.toList());
        // 先计算各批次可用量,避免边扣边算导致判断不一致。
        Map<String, BigDecimal> availableQuantityMap = new LinkedHashMap<>();
        BigDecimal totalAvailableQuantity = BigDecimal.ZERO;
        // 遍历批次,计算每个批次可用库存。
        for (String currentBatchNo : batchNoList) {
            // 查询当前规格+批次的库存记录。
            StockInventory stockInventory = stockInventoryMapper.selectOne(buildStockWrapper(productModelId, currentBatchNo));
        // 按先进先出顺序计算每个批次可用库存。
        for (String currentBatchNo : fifoBatchNoList) {
            StockInventory stockInventory = inventoryMap.get(currentBatchNo);
            BigDecimal availableQuantity = BigDecimal.ZERO;
            if (stockInventory != null) {
                availableQuantity = defaultDecimal(stockInventory.getQualitity())
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -34,7 +34,9 @@
import com.ruoyi.production.service.ProductionOrderService;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.stock.mapper.StockInRecordMapper;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.pojo.StockInRecord;
import com.ruoyi.stock.pojo.StockInventory;
import com.ruoyi.technology.mapper.*;
import com.ruoyi.technology.pojo.*;
@@ -70,6 +72,7 @@
    private final QualityInspectFileMapper qualityInspectFileMapper;
    private final ProductionPlanMapper productionPlanMapper;
    private final StockInventoryMapper stockInventoryMapper;
    private final StockInRecordMapper stockInRecordMapper;
    private final StorageAttachmentMapper storageAttachmentMapper;
    private final StorageBlobMapper storageBlobMapper;
    private final SalesLedgerMapper salesLedgerMapper;
@@ -1144,12 +1147,41 @@
            List<StockInventory> stockList = stockInventoryMapper.selectList(
                    Wrappers.<StockInventory>lambdaQuery()
                            .in(StockInventory::getProductModelId, productModelIds));
            // 各批次首次入库时间,用于先进先出排序(历史数据导入时库存行的 create_time 可能相同)
            Map<Long, Map<String, LocalDateTime>> firstInTimeMap = new HashMap<>();
            List<StockInRecord> stockInRecords = stockInRecordMapper.selectList(
                    Wrappers.<StockInRecord>lambdaQuery()
                            .in(StockInRecord::getProductModelId, productModelIds)
                            .isNotNull(StockInRecord::getBatchNo));
            for (StockInRecord stockInRecord : stockInRecords) {
                if (stockInRecord == null || stockInRecord.getProductModelId() == null
                        || stockInRecord.getBatchNo() == null || stockInRecord.getCreateTime() == null) {
                    continue;
                }
                firstInTimeMap.computeIfAbsent(stockInRecord.getProductModelId(), key -> new HashMap<>())
                        .merge(stockInRecord.getBatchNo(), stockInRecord.getCreateTime(),
                                (a, b) -> a.isBefore(b) ? a : b);
            }
            // 库存行按所属批次的首次入库时间升序,保证领料下拉框是先进先出顺序
            stockList.sort(Comparator
                    .comparing((StockInventory stockItem) -> {
                        Map<String, LocalDateTime> modelBatchTimeMap = firstInTimeMap.get(stockItem.getProductModelId());
                        return modelBatchTimeMap == null ? null : modelBatchTimeMap.get(stockItem.getBatchNo());
                    }, Comparator.nullsLast(Comparator.naturalOrder()))
                    .thenComparing(StockInventory::getCreateTime, Comparator.nullsLast(Comparator.naturalOrder()))
                    .thenComparing(stockItem -> stockItem.getId() == null ? Long.MAX_VALUE : stockItem.getId()));
            for (StockInventory stockItem : stockList) {
                if (stockItem == null || stockItem.getProductModelId() == null) {
                    continue;
                }
                Long productModelId = stockItem.getProductModelId();
                stockQuantityMap.merge(productModelId, defaultDecimal(stockItem.getQualitity()), BigDecimal::add);
                // 已无可用库存(数量扣完或被锁定)的批次不再带到领料下拉框
                BigDecimal availableQuantity = defaultDecimal(stockItem.getQualitity())
                        .subtract(defaultDecimal(stockItem.getLockedQuantity()));
                if (availableQuantity.compareTo(BigDecimal.ZERO) <= 0) {
                    continue;
                }
                String batchNo = stockItem.getBatchNo();
                if (batchNo != null && !batchNo.trim().isEmpty()) {
                    stockBatchNoMap.computeIfAbsent(productModelId, key -> new LinkedHashSet<>()).add(batchNo);
src/main/resources/mapper/basic/ProductModelMapper.xml
@@ -54,12 +54,25 @@
    </select>
    <select id="selectBatchNoListByProductModelId" resultType="java.lang.String">
        select distinct si.batch_no
        select si.batch_no
        from stock_inventory si
        left join (
            select sir.batch_no, min(sir.create_time) as first_in_time
            from stock_in_record sir
            where sir.product_model_id = #{productModelId}
              and sir.batch_no is not null
              and sir.batch_no != ''
            group by sir.batch_no
        ) first_in on first_in.batch_no = si.batch_no
        where si.product_model_id = #{productModelId}
          and si.batch_no is not null
          and si.batch_no != ''
        order by si.batch_no
          and si.qualitity - IFNULL(si.locked_quantity, 0) > 0
        group by si.batch_no
        order by MIN(first_in.first_in_time) is null,
                 MIN(first_in.first_in_time),
                 MIN(si.create_time),
                 MIN(si.batch_no)
    </select>
    <select id="selectLatestRecord" resultType="com.ruoyi.basic.pojo.ProductModel">
            SELECT * FROM product_model