feat: 支持按箱号展开可用库存子卷

This commit is contained in:
zhouz
2026-07-22 14:36:21 +08:00
parent eb35c99966
commit 1fcc152dca
9 changed files with 246 additions and 8 deletions

View File

@@ -38,6 +38,22 @@ public class IostorInvController {
@Resource @Resource
private IostorInvService iostorInvService; private IostorInvService iostorInvService;
@GetMapping("/availableInventoryPage")
@Operation(summary = "获得可用库存分页")
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:query')")
public CommonResult<PageResult<AvailableInventoryRespVO>> getAvailableInventoryPage(
@Valid AvailableInventoryPageReqVO reqVO) {
return success(iostorInvService.getAvailableInventoryPage(reqVO));
}
@PostMapping("/expandAvailableInventory")
@Operation(summary = "按箱号展开全部可用子卷")
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:query')")
public CommonResult<List<AvailableInventoryRespVO>> expandAvailableInventory(
@Valid @RequestBody ExpandAvailableInventoryReqVO reqVO) {
return success(iostorInvService.expandAvailableInventory(reqVO));
}
@PostMapping("/create") @PostMapping("/create")
@Operation(summary = "创建出入库单主表") @Operation(summary = "创建出入库单主表")
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:create')") @PreAuthorize("@ss.hasPermission('wms:iostor-inv:create')")

View File

@@ -0,0 +1,27 @@
package cn.code.nl.module.wms.controller.admin.iostorinv.vo;
import cn.code.nl.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Schema(description = "管理后台 - 可用库存分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class AvailableInventoryPageReqVO extends PageParam {
@Schema(description = "仓库标识", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "仓库标识不能为空")
private String storId;
@Schema(description = "物料编码")
private String materialCode;
@Schema(description = "箱号")
private String vehicleCode;
@Schema(description = "批次")
private String pcsn;
}

View File

@@ -0,0 +1,41 @@
package cn.code.nl.module.wms.controller.admin.iostorinv.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - 可用库存 Response VO")
@Data
public class AvailableInventoryRespVO {
@Schema(description = "箱号")
private String vehicleCode;
@Schema(description = "批次")
private String pcsn;
@Schema(description = "物料标识")
private String materialId;
@Schema(description = "物料编码")
private String materialCode;
@Schema(description = "物料名称")
private String materialName;
@Schema(description = "可用数量")
private BigDecimal availableQty;
@Schema(description = "计量单位标识")
private String qtyUnitId;
@Schema(description = "计量单位名称")
private String qtyUnitName;
@Schema(description = "来源单据号")
private String extCode;
@Schema(description = "来源单据类型")
private String extType;
@Schema(description = "来源单据明细号")
private String extDtlCode;
@Schema(description = "仓库标识")
private String storId;
@Schema(description = "仓库编码")
private String storCode;
@Schema(description = "仓库名称")
private String storName;
}

View File

@@ -0,0 +1,21 @@
package cn.code.nl.module.wms.controller.admin.iostorinv.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 按箱号展开可用库存 Request VO")
@Data
public class ExpandAvailableInventoryReqVO {
@Schema(description = "仓库标识", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "仓库标识不能为空")
private String storId;
@Schema(description = "箱号列表", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "箱号列表不能为空")
private List<String> vehicleCodes;
}

View File

@@ -3,10 +3,15 @@ package cn.code.nl.module.wms.dal.mysql.iostorinvdtl;
import java.util.*; import java.util.*;
import cn.code.nl.framework.common.pojo.PageResult; import cn.code.nl.framework.common.pojo.PageResult;
import cn.code.nl.framework.mybatis.core.util.MyBatisUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import cn.code.nl.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.code.nl.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.code.nl.framework.mybatis.core.mapper.BaseMapperX; import cn.code.nl.framework.mybatis.core.mapper.BaseMapperX;
import cn.code.nl.module.wms.dal.dataobject.iostorinvdtl.IostorinvDtlDO; import cn.code.nl.module.wms.dal.dataobject.iostorinvdtl.IostorinvDtlDO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryPageReqVO;
import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryRespVO;
import cn.code.nl.module.wms.controller.admin.iostorinvdtl.vo.*; import cn.code.nl.module.wms.controller.admin.iostorinvdtl.vo.*;
/** /**
@@ -17,6 +22,27 @@ import cn.code.nl.module.wms.controller.admin.iostorinvdtl.vo.*;
@Mapper @Mapper
public interface IostorinvDtlMapper extends BaseMapperX<IostorinvDtlDO> { public interface IostorinvDtlMapper extends BaseMapperX<IostorinvDtlDO> {
/**
* 分页查询指定仓库的可用库存。
*/
default PageResult<AvailableInventoryRespVO> selectAvailableInventoryPage(AvailableInventoryPageReqVO reqVO) {
Page<AvailableInventoryRespVO> page = selectAvailableInventoryPage(
MyBatisUtils.buildPage(reqVO), reqVO);
return new PageResult<>(page.getRecords(), page.getTotal());
}
/**
* 执行可用库存分页 SQL。
*/
Page<AvailableInventoryRespVO> selectAvailableInventoryPage(
Page<AvailableInventoryRespVO> page, @Param("reqVO") AvailableInventoryPageReqVO reqVO);
/**
* 查询指定仓库、指定箱号下的全部可用子卷。
*/
List<AvailableInventoryRespVO> selectAvailableInventoryByVehicleCodes(
@Param("storId") String storId, @Param("vehicleCodes") Collection<String> vehicleCodes);
default PageResult<IostorinvDtlDO> selectPage(IostorinvDtlPageReqVO reqVO) { default PageResult<IostorinvDtlDO> selectPage(IostorinvDtlPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<IostorinvDtlDO>() return selectPage(reqVO, new LambdaQueryWrapperX<IostorinvDtlDO>()
.eqIfPresent(IostorinvDtlDO::getIostorinvId, reqVO.getIostorinvId()) .eqIfPresent(IostorinvDtlDO::getIostorinvId, reqVO.getIostorinvId())

View File

@@ -22,6 +22,22 @@ public interface IostorInvService {
*/ */
String createOutbound(@Valid IostorInvCreateReqVO reqVO); String createOutbound(@Valid IostorInvCreateReqVO reqVO);
/**
* 获得指定仓库的可用库存分页。
*
* @param reqVO 分页查询条件
* @return 可用库存分页
*/
PageResult<AvailableInventoryRespVO> getAvailableInventoryPage(@Valid AvailableInventoryPageReqVO reqVO);
/**
* 按箱号展开指定仓库中的全部可用子卷。
*
* @param reqVO 展开条件
* @return 可用子卷列表
*/
List<AvailableInventoryRespVO> expandAvailableInventory(@Valid ExpandAvailableInventoryReqVO reqVO);
/** /**
* 创建出入库单主表 * 创建出入库单主表
* *

View File

@@ -14,6 +14,7 @@ import cn.code.nl.framework.common.pojo.PageParam;
import cn.code.nl.framework.common.util.object.BeanUtils; import cn.code.nl.framework.common.util.object.BeanUtils;
import cn.code.nl.module.wms.dal.mysql.iostorinv.IostorInvMapper; import cn.code.nl.module.wms.dal.mysql.iostorinv.IostorInvMapper;
import cn.code.nl.module.wms.dal.mysql.iostorinvdtl.IostorinvDtlMapper;
import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.code.nl.framework.common.util.collection.CollectionUtils.convertList; import static cn.code.nl.framework.common.util.collection.CollectionUtils.convertList;
@@ -32,11 +33,26 @@ public class IostorInvServiceImpl implements IostorInvService {
@Resource @Resource
private IostorInvMapper iostorInvMapper; private IostorInvMapper iostorInvMapper;
@Resource
private IostorinvDtlMapper iostorinvDtlMapper;
@Override @Override
public String createOutbound(IostorInvCreateReqVO reqVO) { public String createOutbound(IostorInvCreateReqVO reqVO) {
throw new UnsupportedOperationException("出库单聚合保存功能尚未实现"); throw new UnsupportedOperationException("出库单聚合保存功能尚未实现");
} }
@Override
public PageResult<AvailableInventoryRespVO> getAvailableInventoryPage(AvailableInventoryPageReqVO reqVO) {
return iostorinvDtlMapper.selectAvailableInventoryPage(reqVO);
}
@Override
public List<AvailableInventoryRespVO> expandAvailableInventory(ExpandAvailableInventoryReqVO reqVO) {
// 用户可以独立勾选子卷;按涉及箱号去重后,由数据库展开箱内全部可用子卷。
Set<String> vehicleCodes = new LinkedHashSet<>(reqVO.getVehicleCodes());
return iostorinvDtlMapper.selectAvailableInventoryByVehicleCodes(reqVO.getStorId(), vehicleCodes);
}
@Override @Override
public String createIostorInv(IostorInvSaveReqVO createReqVO) { public String createIostorInv(IostorInvSaveReqVO createReqVO) {
// 插入 // 插入

View File

@@ -2,11 +2,62 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.code.nl.module.wms.dal.mysql.iostorinvdtl.IostorinvDtlMapper"> <mapper namespace="cn.code.nl.module.wms.dal.mysql.iostorinvdtl.IostorinvDtlMapper">
<!-- <sql id="availableInventoryColumns">
一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。 gp.vehicle_code AS vehicle_code,
无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。 gp.pcsn AS pcsn,
代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。 gp.material_id AS material_id,
文档可见https://www.iocoder.cn/MyBatis/x-plugins/ gp.material_code AS material_code,
--> mb.material_name AS material_name,
gp.qty - gp.frozen_qty AS available_qty,
gp.qty_unit_id AS qty_unit_id,
gp.qty_unit_name AS qty_unit_name,
gp.ext_code AS ext_code,
gp.ext_type AS ext_type,
gp.ext_dtl_code AS ext_dtl_code,
sa.stor_id AS stor_id,
sa.stor_code AS stor_code,
sa.stor_name AS stor_name
</sql>
<select id="selectAvailableInventoryPage"
resultType="cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryRespVO">
SELECT <include refid="availableInventoryColumns"/>
FROM wms_group_plate gp
INNER JOIN wms_structattr sa ON sa.storagevehicle_code = gp.vehicle_code
LEFT JOIN base_materialbase mb ON mb.material_id = gp.material_id AND mb.is_deleted = 0
WHERE sa.stor_id = #{reqVO.storId}
AND gp.status = '可用'
AND gp.qty - gp.frozen_qty &gt; 0
AND gp.is_deleted = 0
AND sa.is_deleted = 0
<if test="reqVO.materialCode != null and reqVO.materialCode != ''">
AND gp.material_code LIKE CONCAT('%', #{reqVO.materialCode}, '%')
</if>
<if test="reqVO.vehicleCode != null and reqVO.vehicleCode != ''">
AND gp.vehicle_code LIKE CONCAT('%', #{reqVO.vehicleCode}, '%')
</if>
<if test="reqVO.pcsn != null and reqVO.pcsn != ''">
AND gp.pcsn LIKE CONCAT('%', #{reqVO.pcsn}, '%')
</if>
ORDER BY gp.vehicle_code, gp.group_id
</select>
<select id="selectAvailableInventoryByVehicleCodes"
resultType="cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryRespVO">
SELECT <include refid="availableInventoryColumns"/>
FROM wms_group_plate gp
INNER JOIN wms_structattr sa ON sa.storagevehicle_code = gp.vehicle_code
LEFT JOIN base_materialbase mb ON mb.material_id = gp.material_id AND mb.is_deleted = 0
WHERE sa.stor_id = #{storId}
AND gp.status = '可用'
AND gp.qty - gp.frozen_qty &gt; 0
AND gp.is_deleted = 0
AND sa.is_deleted = 0
AND gp.vehicle_code IN
<foreach collection="vehicleCodes" item="vehicleCode" open="(" separator="," close=")">
#{vehicleCode}
</foreach>
ORDER BY gp.vehicle_code, gp.group_id
</select>
</mapper> </mapper>

View File

@@ -1,17 +1,22 @@
package cn.code.nl.module.wms.service.iostorinv; package cn.code.nl.module.wms.service.iostorinv;
import cn.code.nl.module.wms.controller.admin.iostorinv.vo.IostorInvCreateReqVO; import cn.code.nl.module.wms.controller.admin.iostorinv.vo.IostorInvCreateReqVO;
import cn.code.nl.module.wms.controller.admin.iostorinv.vo.AvailableInventoryRespVO;
import cn.code.nl.module.wms.controller.admin.iostorinv.vo.ExpandAvailableInventoryReqVO;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.validation.ConstraintViolationException; import jakarta.validation.ConstraintViolationException;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Disabled;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest @SpringBootTest
@ActiveProfiles("local") @ActiveProfiles("local")
@@ -34,4 +39,23 @@ class IostorInvServiceLocalSpringTest {
assertTrue(exception.getConstraintViolations().stream() assertTrue(exception.getConstraintViolations().stream()
.anyMatch(violation -> violation.getPropertyPath().toString().contains("details"))); .anyMatch(violation -> violation.getPropertyPath().toString().contains("details")));
} }
/**
* 本地库数据前置:仓库 stor-1 的 BOX-001 有两个可用子卷,另一仓库也存在 BOX-001。
* 当前仓库没有可移植的 WMS 本地测试数据集,避免向开发库写入固定业务数据,准备好上述数据后可手动启用。
*/
@Test
@Disabled("需要 local 数据库预置跨仓库 BOX-001 测试数据")
void expandAvailableInventoryShouldReturnAllChildrenOnceAndOnlyFromRequestedWarehouse() {
ExpandAvailableInventoryReqVO reqVO = new ExpandAvailableInventoryReqVO();
reqVO.setStorId("stor-1");
reqVO.setVehicleCodes(List.of("BOX-001", "BOX-001"));
List<AvailableInventoryRespVO> result = iostorInvService.expandAvailableInventory(reqVO);
assertEquals(2, result.size());
assertTrue(result.stream().allMatch(item -> "BOX-001".equals(item.getVehicleCode())));
assertTrue(result.stream().allMatch(item -> "stor-1".equals(item.getStorId())));
assertEquals(2, result.stream().map(AvailableInventoryRespVO::getPcsn).distinct().count());
}
} }