gaoluyang
2026-10-29 23317ed94a5e2d9b19c16627a70f9c7b69c908a2
feat: 发货支持无库存批号(未启用仓储模块的租户)

1. ShippingInfoServiceImpl.add 明细空值安全,新增 deductOrRecordStock:有库存批号扣库存,无批号只登记出库流水
2. StockUtils 新增 recordStockOutWithoutInventory,登记出库流水但不校验/扣减库存
3. StockUtils.shipmentStatus:无库存批号的发货审批通过后直接置出库流水为已审核,否则台账已发货数量统计不到
4. ShippingProductDetailMapper 明细查询用 spd.product_model_id 兜底,无批号时也能显示产品名称和规格

Co-Authored-By: Claude Code <noreply@anthropic.com>
已修改3个文件
70 ■■■■■ 文件已修改
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/ShippingProductDetailMapper.xml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java
@@ -4,9 +4,13 @@
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.ruoyi.common.enums.ReviewStatusEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordOutMapper;
import com.ruoyi.sales.mapper.ShippingProductDetailMapper;
import com.ruoyi.sales.pojo.ShippingProductDetail;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.dto.StockOutRecordDto;
import com.ruoyi.stock.dto.StockUninventoryDto;
import com.ruoyi.stock.pojo.StockInRecord;
import com.ruoyi.stock.pojo.StockOutRecord;
@@ -31,6 +35,7 @@
    private final StockInventoryService stockInventoryService;
    private final StockInRecordService stockInRecordService;
    private final StockOutRecordService stockOutRecordService;
    private final ShippingProductDetailMapper shippingProductDetailMapper;
    /**
     * 不合格入库
@@ -154,15 +159,47 @@
    }
    /**
     * 发货审批状态更改
     * 只登记出库流水,不校验、不扣减库存。
     * 用于未启用仓储模块、发货没有库存批号的场景。
     *
     * @param productModelId
     * @param quantity
     * @param recordType
     * @param recordId
     * @param batchNo
     */
    public void recordStockOutWithoutInventory(Long productModelId, BigDecimal quantity, String recordType, Long recordId, String batchNo) {
        StockOutRecordDto stockOutRecordDto = new StockOutRecordDto();
        stockOutRecordDto.setRecordId(recordId);
        stockOutRecordDto.setRecordType(String.valueOf(recordType));
        stockOutRecordDto.setStockOutNum(quantity);
        stockOutRecordDto.setProductModelId(productModelId);
        stockOutRecordDto.setBatchNo(batchNo);
        stockOutRecordDto.setType("0");
        stockOutRecordService.add(stockOutRecordDto);
    }
    /**
     * 发货审批状态更改。
     * 没有库存批号的发货(未启用仓储模块)没有出库审核环节,审批通过即视为已出库,
     * 否则发货数量统计不到。
     *
     * @param recordType
     * @param recordId
     */
    public void shipmentStatus(String recordType, Long recordId) {
        Long inventoryBatchCount = shippingProductDetailMapper.selectCount(new LambdaQueryWrapper<ShippingProductDetail>()
                .eq(ShippingProductDetail::getShippingInfoId, recordId)
                .isNotNull(ShippingProductDetail::getStockInventoryId));
        boolean withoutInventoryBatch = StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode().equals(recordType)
                && inventoryBatchCount == 0;
        Integer approvalStatus = withoutInventoryBatch
                ? ReviewStatusEnum.APPROVED.getCode()
                : ReviewStatusEnum.PENDING_REVIEW.getCode();
        LambdaQueryWrapper<StockOutRecord> queryWrapper = new LambdaQueryWrapper<StockOutRecord>().eq(StockOutRecord::getRecordType, recordType)
                .eq(StockOutRecord::getRecordId, recordId);
        stockOutRecordService.list(queryWrapper).stream().forEach(stockOutRecord -> {
            stockOutRecord.setApprovalStatus(0);
            stockOutRecord.setApprovalStatus(approvalStatus);
            stockOutRecordService.updateById(stockOutRecord);
        });
    }
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java
@@ -85,7 +85,7 @@
                throw new RuntimeException("发货信息不存在");
            }
            for (ShippingProductDetail shippingProductDetail : shippingProductDetails) {
                stockUtils.substractStock(shippingProductDetail.getProductModelId(), shippingProductDetail.getQuantity(), StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode(), req.getId(), shippingProductDetail.getBatchNo());
                deductOrRecordStock(shippingProductDetail, req.getId());
            }
        }
        byId.setExpressNumber(req.getExpressNumber());
@@ -143,16 +143,31 @@
    @Override
    public boolean add(ShippingInfoDto req) {
        this.save(req);
        req.getBatchNoDetailList().forEach(item -> item.setShippingInfoId(req.getId()));
        shippingProductDetailMapper.insert(req.getBatchNoDetailList());
        for (ShippingProductDetail shippingProductDetail : req.getBatchNoDetailList()) {
            stockUtils.substractStock(shippingProductDetail.getProductModelId(), shippingProductDetail.getQuantity(), StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode(), req.getId(), shippingProductDetail.getBatchNo());
        List<ShippingProductDetail> detailList = req.getBatchNoDetailList();
        if (CollectionUtils.isNotEmpty(detailList)) {
            detailList.forEach(item -> item.setShippingInfoId(req.getId()));
            shippingProductDetailMapper.insert(detailList);
            for (ShippingProductDetail shippingProductDetail : detailList) {
                deductOrRecordStock(shippingProductDetail, req.getId());
            }
        }
        // 保存文件
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.IMAGE, RecordTypeEnum.SHIPPING_INFO, req.getId(), req.getStorageBlobDTOs());
        return true;
    }
    /**
     * 有库存批号的发货扣减库存;没有库存批号(未启用仓储模块)的发货只登记出库流水。
     */
    private void deductOrRecordStock(ShippingProductDetail detail, Long shippingInfoId) {
        String recordType = StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode();
        if (detail.getStockInventoryId() == null) {
            stockUtils.recordStockOutWithoutInventory(detail.getProductModelId(), detail.getQuantity(), recordType, shippingInfoId, detail.getBatchNo());
        } else {
            stockUtils.substractStock(detail.getProductModelId(), detail.getQuantity(), recordType, shippingInfoId, detail.getBatchNo());
        }
    }
    @Override
    public List<ShippingProductDetailDto> getDetail(Long id) {
        return shippingProductDetailMapper.getDetail(id);
src/main/resources/mapper/sales/ShippingProductDetailMapper.xml
@@ -14,7 +14,7 @@
        select si.batch_no, pm.model as specification_model, p.product_name, spd.quantity as delivery_quantity
        from shipping_product_detail spd
                 left join stock_inventory si on si.id = spd.stock_inventory_id
                 left join product_model pm on pm.id = si.product_model_id
                 left join product_model pm on pm.id = IFNULL(si.product_model_id, spd.product_model_id)
                 left join product p on p.id = pm.product_id
        where spd.shipping_info_id = #{id}
    </select>
@@ -23,7 +23,7 @@
         from shipping_product_detail spd
                  left join shipping_info sp on sp.id = spd.shipping_info_id
                  left join stock_inventory si on si.id = spd.stock_inventory_id
                  left join product_model pm on pm.id = si.product_model_id
                  left join product_model pm on pm.id = IFNULL(si.product_model_id, spd.product_model_id)
                  left join product p on p.id = pm.product_id
         where sp.shipping_no = #{shippingNo}
    </select>