Files
klp-oa/klp-wms/src/main/java/com/klp/service/impl/WmsStockIoServiceImpl.java
Joshi 408ce31776 refactor(wms): 优化退库操作流程
- 移除了不必要的验证逻辑,简化了代码结构
- 使用 stream API 优化了原出库明细的查找和已退库数量的计算
-调整了退库数量验证的逻辑,提高了用户体验
-优化了单位的获取逻辑,增加了默认值
2025-08-12 10:31:19 +08:00

480 lines
20 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.klp.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.klp.common.core.page.TableDataInfo;
import com.klp.common.core.domain.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.klp.common.utils.StringUtils;
import com.klp.domain.*;
import com.klp.domain.vo.WmsStockIoDetailVo;
import com.klp.mapper.*;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import com.klp.domain.bo.WmsStockIoBo;
import com.klp.domain.vo.WmsStockIoVo;
import com.klp.service.IWmsStockIoService;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.*;
import com.klp.common.exception.ServiceException;
import com.klp.domain.bo.WmsStockIoWithDetailBo;
import com.klp.domain.bo.WmsStockIoDetailBo;
import javax.annotation.Resource;
/**
* 出入库单主Service业务层处理
*
* @author Joshi
* @date 2025-07-18
*/
@RequiredArgsConstructor
@Service
public class WmsStockIoServiceImpl implements IWmsStockIoService {
private final WmsStockIoMapper baseMapper;
private final WmsStockIoDetailMapper stockIoDetailMapper;
private final WmsStockMapper stockMapper;
private final WmsProductMapper productMapper;
private final WmsRawMaterialMapper rawMaterialMapper;
private final WmsWarehouseMapper warehouseMapper;
@Resource
private WmsStockLogMapper stockLogMapper;
/**
* 查询出入库单主
*/
@Override
public WmsStockIoVo queryById(Long stockIoId){
return baseMapper.selectVoById(stockIoId);
}
/**
* 查询出入库单主列表
*/
@Override
public TableDataInfo<WmsStockIoVo> queryPageList(WmsStockIoBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<WmsStockIo> lqw = buildQueryWrapper(bo);
Page<WmsStockIoVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询出入库单主列表
*/
@Override
public List<WmsStockIoVo> queryList(WmsStockIoBo bo) {
LambdaQueryWrapper<WmsStockIo> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<WmsStockIo> buildQueryWrapper(WmsStockIoBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<WmsStockIo> lqw = Wrappers.lambdaQuery();
lqw.eq(StringUtils.isNotBlank(bo.getStockIoCode()), WmsStockIo::getStockIoCode, bo.getStockIoCode());
lqw.eq(StringUtils.isNotBlank(bo.getIoType()), WmsStockIo::getIoType, bo.getIoType());
lqw.eq(StringUtils.isNotBlank(bo.getBizType()), WmsStockIo::getBizType, bo.getBizType());
lqw.eq(bo.getStatus() != null, WmsStockIo::getStatus, bo.getStatus());
return lqw;
}
/**
* 新增出入库单主
*/
@Override
public Boolean insertByBo(WmsStockIoBo bo) {
WmsStockIo add = BeanUtil.toBean(bo, WmsStockIo.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setStockIoId(add.getStockIoId());
}
return flag;
}
/**
* 修改出入库单主
*/
@Override
public Boolean updateByBo(WmsStockIoBo bo) {
WmsStockIo update = BeanUtil.toBean(bo, WmsStockIo.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(WmsStockIo entity){
//TODO 做一些数据校验,如唯一约束
}
/**
* 批量删除出入库单主
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteBatchIds(ids) > 0;
}
/**
* 批量新增主表和明细
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void addWithDetail(WmsStockIoWithDetailBo bo) {
// 插入主表
WmsStockIo stockIo = BeanUtil.toBean(bo, WmsStockIo.class);
stockIo.setParentId(bo.getStockIoId());
baseMapper.insert(stockIo);
// 插入明细
if (bo.getDetails() != null && !bo.getDetails().isEmpty()) {
for (WmsStockIoDetailBo detailBo : bo.getDetails()) {
WmsStockIoDetail detail = BeanUtil.toBean(detailBo, WmsStockIoDetail.class);
detail.setStockIoId(stockIo.getStockIoId());
stockIoDetailMapper.insert(detail);
}
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean scanInStockByBo(WmsStockIoDetailBo bo) {
String unit = bo.getUnit();
// 如果unit为空自动查item表补全
if (unit == null || unit.trim().isEmpty()) {
if ("product".equals(bo.getItemType())) {
WmsProduct p = productMapper.selectById(bo.getItemId());
unit = p != null ? p.getUnit() : null;
} else if ("raw_material".equals(bo.getItemType())) {
WmsRawMaterial r = rawMaterialMapper.selectById(bo.getItemId());
unit = r != null ? r.getUnit() : null;
}
}
if (unit == null || unit.trim().isEmpty()) {
throw new RuntimeException("未能获取到单位");
}
// 入库操作
changeStock(bo.getWarehouseId(), bo.getItemType(), bo.getItemId(), bo.getQuantity(), true, unit);
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean scanOutStockByBo(WmsStockIoDetailBo bo) {
String unit = bo.getUnit();
// 如果unit为空自动查item表补全
if (unit == null || unit.trim().isEmpty()) {
if ("product".equals(bo.getItemType())) {
WmsProduct p = productMapper.selectById(bo.getItemId());
unit = p != null ? p.getUnit() : null;
} else if ("raw_material".equals(bo.getItemType())) {
WmsRawMaterial r = rawMaterialMapper.selectById(bo.getItemId());
unit = r != null ? r.getUnit() : null;
}
}
if (unit == null || unit.trim().isEmpty()) {
throw new RuntimeException("未能获取到单位");
}
// 出库操作
changeStock(bo.getWarehouseId(), bo.getItemType(), bo.getItemId(), bo.getQuantity(), false, unit);
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean scanReturnStockByBo(WmsStockIoBo bo) {
// 退库操作根据明细ID查询明细根据parent_id获取原出库单明细验证退库数量
if (bo.getStockIoId() == null) {
throw new ServiceException("退库明细ID不能为空");
}
// 1. 根据明细ID查询退库单明细
WmsStockIoDetail returnDetail = stockIoDetailMapper.selectById(bo.getStockIoId());
if (returnDetail == null) {
throw new ServiceException("退库明细不存在");
}
// 2. 获取退库单主表信息
WmsStockIo returnStockIo = baseMapper.selectById(returnDetail.getStockIoId());
if (returnStockIo == null || returnStockIo.getParentId() == null) {
throw new ServiceException("退库单不存在或未关联原出库单");
}
// 验证退库单类型必须是退库类型
if (!"withdraw".equals(returnStockIo.getIoType())) {
throw new ServiceException("单据类型必须是退库类型");
}
// 3. 获取原出库单信息
WmsStockIo originalOutStockIo = baseMapper.selectById(returnStockIo.getParentId());
if (originalOutStockIo == null || !"out".equals(originalOutStockIo.getIoType())) {
throw new ServiceException("原出库单不存在或类型错误");
}
// 4. 查找对应的原出库明细
List<WmsStockIoDetail> originalOutDetails = stockIoDetailMapper.selectList(
Wrappers.<WmsStockIoDetail>lambdaQuery()
.eq(WmsStockIoDetail::getStockIoId, returnStockIo.getParentId())
);
WmsStockIoDetail originalOutDetail = originalOutDetails.stream()
.filter(detail -> detail.getItemType().equals(returnDetail.getItemType()) &&
detail.getItemId().equals(returnDetail.getItemId()) &&
detail.getWarehouseId().equals(returnDetail.getWarehouseId()))
.findFirst()
.orElse(null);
if (originalOutDetail == null) {
throw new ServiceException("未找到对应的原出库明细");
}
// 5. 验证退库数量不能超过原出库数量
BigDecimal totalReturnedQty = baseMapper.selectList(
Wrappers.<WmsStockIo>lambdaQuery()
.eq(WmsStockIo::getParentId, returnStockIo.getParentId())
.eq(WmsStockIo::getIoType, "withdraw")
.ne(WmsStockIo::getStockIoId, returnStockIo.getStockIoId())
).stream()
.flatMap(returnStockIoItem -> stockIoDetailMapper.selectList(
Wrappers.<WmsStockIoDetail>lambdaQuery()
.eq(WmsStockIoDetail::getStockIoId, returnStockIoItem.getStockIoId())
.eq(WmsStockIoDetail::getItemType, returnDetail.getItemType())
.eq(WmsStockIoDetail::getItemId, returnDetail.getItemId())
.eq(WmsStockIoDetail::getWarehouseId, returnDetail.getWarehouseId())
).stream())
.map(WmsStockIoDetail::getQuantity)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal remainingQty = originalOutDetail.getQuantity().subtract(totalReturnedQty);
if (returnDetail.getQuantity().compareTo(remainingQty) > 0) {
throw new ServiceException("退库数量超过原出库数量,剩余可退数量:" + remainingQty);
}
// 6. 执行退库操作(增加库存)
String unit = returnDetail.getUnit();
if (unit == null || unit.trim().isEmpty()) {
if ("product".equals(returnDetail.getItemType())) {
WmsProduct p = productMapper.selectById(returnDetail.getItemId());
unit = p != null ? p.getUnit() : "";
} else if ("raw_material".equals(returnDetail.getItemType())) {
WmsRawMaterial r = rawMaterialMapper.selectById(returnDetail.getItemId());
unit = r != null ? r.getUnit() : "";
}
}
// 执行入库操作(退库就是入库)
changeStock(returnDetail.getWarehouseId(), returnDetail.getItemType(),
returnDetail.getItemId(), returnDetail.getQuantity(), true, unit);
// 记录退库操作日志
WmsStockLog returnLog = new WmsStockLog();
returnLog.setWarehouseId(returnDetail.getWarehouseId());
returnLog.setItemType(returnDetail.getItemType());
returnLog.setItemId(returnDetail.getItemId());
returnLog.setChangeQty(returnDetail.getQuantity());
returnLog.setChangeType("退库");
returnLog.setChangeTime(new Date());
stockLogMapper.insert(returnLog);
return true;
}
/**
* 审核出入库/移库单,变更库存,含库存校验
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean auditStockIo(Long stockIoId) {
WmsStockIo stockIo = baseMapper.selectById(stockIoId);
if (stockIo == null || stockIo.getStatus() == null || stockIo.getStatus() != 1) {
// 只允许审核“已提交”状态的单据
throw new RuntimeException("单据不存在或状态不允许审核");
}
List<WmsStockIoDetail> details = stockIoDetailMapper.selectList(
Wrappers.<WmsStockIoDetail>lambdaQuery().eq(WmsStockIoDetail::getStockIoId, stockIoId)
);
if (details == null || details.isEmpty()) {
throw new RuntimeException("单据明细不能为空");
}
for (WmsStockIoDetail detail : details) {
String ioType = stockIo.getIoType();
if ("in".equals(ioType)) {
// 入库:目标库位库存增加
changeStock(detail.getWarehouseId(), detail.getItemType(), detail.getItemId(), detail.getQuantity(), true, detail.getUnit());
} else if ("out".equals(ioType)) {
// 出库:目标库位库存减少
changeStock(detail.getWarehouseId(), detail.getItemType(), detail.getItemId(), detail.getQuantity(), false, detail.getUnit());
} else if ("transfer".equals(ioType)) {
// 移库fromWarehouseId减少warehouseId增加
if (detail.getFromWarehouseId() == null) {
throw new RuntimeException("移库明细缺少源库位ID");
}
// 先减少源库位
changeStock(detail.getFromWarehouseId(), detail.getItemType(), detail.getItemId(), detail.getQuantity(), false, detail.getUnit());
// 再增加目标库位
changeStock(detail.getWarehouseId(), detail.getItemType(), detail.getItemId(), detail.getQuantity(), true, detail.getUnit());
} else {
throw new RuntimeException("未知的出入库类型");
}
}
// 更新单据状态为已审核2
stockIo.setStatus(2);
return baseMapper.updateById(stockIo) > 0;
}
/**
* 撤销出入库/移库单,库存回滚
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean cancelStockIo(Long stockIoId) {
WmsStockIo stockIo = baseMapper.selectById(stockIoId);
if (stockIo == null || stockIo.getStatus() == null || stockIo.getStatus() != 2) {
throw new ServiceException("只能撤销已审核的单据");
}
List<WmsStockIoDetail> details = stockIoDetailMapper.selectList(
Wrappers.<WmsStockIoDetail>lambdaQuery().eq(WmsStockIoDetail::getStockIoId, stockIoId)
);
if (details == null || details.isEmpty()) {
throw new ServiceException("单据明细不能为空");
}
for (WmsStockIoDetail detail : details) {
String ioType = stockIo.getIoType();
if ("in".equals(ioType)) {
// 入库撤销:目标库位库存减少
// changeStock(detail.getWarehouseId(), detail.getItemType(), detail.getItemId(), detail.getBatchNo(), detail.getQuantity(), false, detail.getUnit());
changeStock(detail.getWarehouseId(), detail.getItemType(), detail.getItemId(), detail.getQuantity(), false, detail.getUnit());
} else if ("out".equals(ioType)) {
// 出库撤销:目标库位库存增加
changeStock(detail.getWarehouseId(), detail.getItemType(), detail.getItemId(), detail.getQuantity(), true, detail.getUnit());
} else if ("transfer".equals(ioType)) {
if (detail.getFromWarehouseId() == null) {
throw new ServiceException("移库明细缺少源库位ID");
}
// 源库位库存增加
changeStock(detail.getFromWarehouseId(), detail.getItemType(), detail.getItemId(), detail.getQuantity(), true, detail.getUnit());
// 目标库位库存减少
changeStock(detail.getWarehouseId(), detail.getItemType(), detail.getItemId(), detail.getQuantity(), false, detail.getUnit());
} else {
throw new ServiceException("未知的出入库类型");
}
}
// 更新单据状态为待审核1
stockIo.setStatus(1);
baseMapper.updateById(stockIo);
return true;
}
/**
* 根据ioType和stockIoId联查明细
*/
@Override
public List<WmsStockIoDetailVo> detailByTypeAndId(String ioType, Long stockIoId) {
WmsStockIo stockIo = baseMapper.selectById(stockIoId);
if (stockIo == null) {
throw new ServiceException("单据不存在");
}
if (!ioType.equals(stockIo.getIoType())) {
throw new ServiceException("单据类型不匹配");
}
List<WmsStockIoDetail> details = stockIoDetailMapper.selectList(
Wrappers.<WmsStockIoDetail>lambdaQuery().eq(WmsStockIoDetail::getStockIoId, stockIoId)
);
if (details == null || details.isEmpty()) {
return java.util.Collections.emptyList();
}
// 转VO
List<WmsStockIoDetailVo> voList = new ArrayList<>();
for (WmsStockIoDetail detail : details) {
WmsStockIoDetailVo vo = new WmsStockIoDetailVo();
BeanUtils.copyProperties(detail, vo);
voList.add(vo);
}
return voList;
}
/**
* 更新出入库单状态
*/
@Override
public Boolean updateStatus(Long stockIoId, Integer status) {
WmsStockIo stockIo = baseMapper.selectById(stockIoId);
if (stockIo == null) {
throw new ServiceException("单据不存在");
}
// 状态流转验证
if (stockIo.getStatus() == 0 && status == 1) {
// 草稿 -> 已提交:需要检查是否有明细
List<WmsStockIoDetail> details = stockIoDetailMapper.selectList(
Wrappers.<WmsStockIoDetail>lambdaQuery().eq(WmsStockIoDetail::getStockIoId, stockIoId)
);
if (details == null || details.isEmpty()) {
throw new ServiceException("单据明细不能为空,无法提交");
}
} else if (stockIo.getStatus() == 1 && status == 0) {
// 已提交 -> 草稿:允许回退
} else {
throw new ServiceException("状态流转不允许");
}
stockIo.setStatus(status);
return baseMapper.updateById(stockIo) > 0;
}
/**
* 库存增减isAdd=true为增加false为减少减少时校验库存是否足够
*/
private void changeStock(Long warehouseId, String itemType, Long itemId, BigDecimal quantity, boolean isAdd, String unit) {
WmsStock stock = stockMapper.selectOne(Wrappers.<WmsStock>lambdaQuery()
.eq(WmsStock::getWarehouseId, warehouseId)
.eq(WmsStock::getItemType, itemType)
.eq(WmsStock::getItemId, itemId)
// .eq(WmsStock::getBatchNo, batchNo)
.last("limit 1"));
if (stock == null) {
if (!isAdd) {
throw new RuntimeException("库存不足,无法出库/移库");
}
// 新增库存记录
stock = new WmsStock();
stock.setWarehouseId(warehouseId);
stock.setItemType(itemType);
stock.setItemId(itemId);
stock.setBatchNo("B-100");
stock.setQuantity(quantity);
stock.setUnit(unit);
stockMapper.insert(stock);
} else {
BigDecimal newQty = isAdd ? stock.getQuantity().add(quantity) : stock.getQuantity().subtract(quantity);
if (newQty.compareTo(BigDecimal.ZERO) < 0) {
throw new RuntimeException("库存不足,无法出库/移库");
}
stock.setQuantity(newQty);
stockMapper.updateById(stock);
}
// 记录库存变更日志
WmsStockLog log = new WmsStockLog();
log.setWarehouseId(warehouseId);
log.setItemType(itemType);
log.setItemId(itemId);
// 变动数量(正=入库,负=出库)
log.setChangeQty(isAdd ? quantity : quantity.negate());
// 变动后的库存数量
log.setAfterQty(stock.getQuantity());
// 变动类型(入库/出库等)
log.setChangeType(isAdd ? "入库" : "出库");
log.setChangeTime(new Date());
stockLogMapper.insert(log);
}
}