Commit d8f9f0d02920483d359e97c6eace5ce8c6a36d6e

Authored by YunaiV
1 parent 10ef8074

feat(wms):调整 check 的实现

yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/enums/ErrorCodeConstants.java
... ... @@ -23,10 +23,12 @@ public interface ErrorCodeConstants {
23 23 ErrorCode ITEM_CATEGORY_PARENT_IS_CHILD = new ErrorCode(1_060_102_004, "不能设置自己的子商品分类为父商品分类");
24 24 ErrorCode ITEM_CATEGORY_HAS_CHILDREN = new ErrorCode(1_060_102_005, "删除失败!请先删除该分类下的子分类!");
25 25 ErrorCode ITEM_CATEGORY_HAS_ITEM = new ErrorCode(1_060_102_006, "删除失败!分类已被商品使用!");
  26 + ErrorCode ITEM_CATEGORY_CODE_DUPLICATE = new ErrorCode(1_060_102_007, "商品分类编号重复");
26 27  
27 28 // ========== WMS 基础数据-商品品牌 1-060-103-000 ==========
28 29 ErrorCode ITEM_BRAND_NOT_EXISTS = new ErrorCode(1_060_103_000, "商品品牌不存在");
29 30 ErrorCode ITEM_BRAND_HAS_ITEM = new ErrorCode(1_060_103_001, "删除失败!品牌已被商品使用!");
  31 + ErrorCode ITEM_BRAND_CODE_DUPLICATE = new ErrorCode(1_060_103_002, "商品品牌编号重复");
30 32  
31 33 // ========== WMS 基础数据-商品 1-060-104-000 ==========
32 34 ErrorCode ITEM_NOT_EXISTS = new ErrorCode(1_060_104_000, "商品不存在");
... ... @@ -75,6 +77,7 @@ public interface ErrorCodeConstants {
75 77 ErrorCode CHECK_ORDER_DETAIL_REQUIRED = new ErrorCode(1_060_203_003, "盘库单至少包含一条明细");
76 78 ErrorCode CHECK_ORDER_STATUS_NOT_DELETABLE = new ErrorCode(1_060_203_005, "盘库单状态不是草稿或已作废,不能删除");
77 79 ErrorCode CHECK_ORDER_DETAIL_NOT_EXISTS = new ErrorCode(1_060_203_006, "盘库单明细不存在");
  80 + ErrorCode CHECK_ORDER_INVENTORY_CHANGED = new ErrorCode(1_060_203_007, "盘库单库存已变化,请重新加载库存后再完成");
78 81  
79 82 // ========== WMS 库存 1-060-300-000 ==========
80 83 ErrorCode INVENTORY_QUANTITY_NOT_ENOUGH = new ErrorCode(1_060_300_000,
... ...
yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImpl.java
... ... @@ -77,10 +77,13 @@ public class WmsInventoryServiceImpl implements WmsInventoryService {
77 77 return;
78 78 }
79 79  
  80 + // 1. 逐条复核账面库存,并计算库存调整和库存流水
80 81 List<WmsInventoryDO> updateInventories = new ArrayList<>(reqDTO.getItems().size());
81 82 List<WmsInventoryHistoryDO> histories = new ArrayList<>(reqDTO.getItems().size());
82 83 for (WmsInventoryCheckReqDTO.Item item : reqDTO.getItems()) {
  84 + // 1.1 锁定或创建库存余额行
83 85 WmsInventoryDO inventory = getOrCreateCheckInventory(item);
  86 + // 1.2 无盈亏时不更新库存,也不生成库存流水
84 87 BigDecimal beforeQuantity = inventory.getQuantity();
85 88 BigDecimal afterQuantity = item.getCheckQuantity();
86 89 if (beforeQuantity.compareTo(afterQuantity) == 0) {
... ... @@ -89,9 +92,12 @@ public class WmsInventoryServiceImpl implements WmsInventoryService {
89 92 updateInventories.add(new WmsInventoryDO().setId(inventory.getId()).setQuantity(afterQuantity));
90 93 histories.add(buildInventoryHistory(reqDTO, item, beforeQuantity, afterQuantity));
91 94 }
  95 +
  96 + // 2.1 批量更新库存余额
92 97 if (CollUtil.isNotEmpty(updateInventories)) {
93 98 inventoryMapper.updateBatch(updateInventories);
94 99 }
  100 + // 2.2 批量写入库存流水
95 101 if (CollUtil.isNotEmpty(histories)) {
96 102 inventoryHistoryService.createInventoryHistoryList(histories);
97 103 }
... ...
yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemSkuService.java
1 1 package cn.iocoder.yudao.module.wms.service.md.item;
2 2  
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku.WmsItemSkuPageReqVO;
3 5 import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku.WmsItemSkuSaveReqVO;
4 6 import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO;
5 7 import jakarta.validation.Valid;
... ... @@ -84,6 +86,14 @@ public interface WmsItemSkuService {
84 86 List<WmsItemSkuDO> getItemSkuListByIds(Collection<Long> ids);
85 87  
86 88 /**
  89 + * 按 SKU 维度分页查询,支持商品 / 品牌 / 分类多表联查筛选。
  90 + *
  91 + * @param pageReqVO 分页与筛选条件
  92 + * @return SKU 分页结果
  93 + */
  94 + PageResult<WmsItemSkuDO> getItemSkuPage(WmsItemSkuPageReqVO pageReqVO);
  95 +
  96 + /**
87 97 * 按编号集合获得 SKU Map
88 98 *
89 99 * @param ids 编号集合
... ...
yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemSkuServiceImpl.java
... ... @@ -2,7 +2,9 @@ package cn.iocoder.yudao.module.wms.service.md.item;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.collection.ListUtil;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 6 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  7 +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku.WmsItemSkuPageReqVO;
6 8 import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku.WmsItemSkuSaveReqVO;
7 9 import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO;
8 10 import cn.iocoder.yudao.module.wms.dal.mysql.md.item.WmsItemSkuMapper;
... ... @@ -139,6 +141,11 @@ public class WmsItemSkuServiceImpl implements WmsItemSkuService {
139 141 return itemSkuMapper.selectByIds(ids);
140 142 }
141 143  
  144 + @Override
  145 + public PageResult<WmsItemSkuDO> getItemSkuPage(WmsItemSkuPageReqVO pageReqVO) {
  146 + return itemSkuMapper.selectPage(pageReqVO);
  147 + }
  148 +
142 149 private void validateItemSkuList(List<WmsItemSkuSaveReqVO> skus) {
143 150 // 校验至少存在一个商品 SKU
144 151 if (CollUtil.isEmpty(skus)) {
... ...
yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderServiceImpl.java
... ... @@ -13,7 +13,7 @@ import cn.iocoder.yudao.module.wms.dal.mysql.order.check.WmsCheckOrderMapper;
13 13 import cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum;
14 14 import cn.iocoder.yudao.module.wms.enums.order.WmsOrderStatusEnum;
15 15 import cn.iocoder.yudao.module.wms.service.inventory.WmsInventoryService;
16   -import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryChangeReqDTO;
  16 +import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryCheckReqDTO;
17 17 import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService;
18 18 import jakarta.annotation.Resource;
19 19 import org.springframework.stereotype.Service;
... ... @@ -21,7 +21,6 @@ import org.springframework.transaction.annotation.Transactional;
21 21 import org.springframework.validation.annotation.Validated;
22 22  
23 23 import java.math.BigDecimal;
24   -import java.util.ArrayList;
25 24 import java.util.List;
26 25  
27 26 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
... ... @@ -108,7 +107,7 @@ public class WmsCheckOrderServiceImpl implements WmsCheckOrderService {
108 107 }
109 108  
110 109 // 3. 盘点库存
111   - changeInventory(order, details);
  110 + inventoryService.checkInventory(buildCheckInventoryReqDTO(order, details));
112 111 }
113 112  
114 113 @Override
... ... @@ -207,29 +206,12 @@ public class WmsCheckOrderServiceImpl implements WmsCheckOrderService {
207 206 return order;
208 207 }
209 208  
210   - /**
211   - * 盘点盘库单对应库存
212   - *
213   - * @param order 盘库单
214   - * @param details 盘库单明细列表
215   - */
216   - private void changeInventory(WmsCheckOrderDO order, List<WmsCheckOrderDetailDO> details) {
217   - List<WmsInventoryChangeReqDTO.Item> items = new ArrayList<>(details.size());
218   - for (WmsCheckOrderDetailDO detail : details) {
219   - BigDecimal differenceQuantity = calculateDifferenceQuantity(detail.getQuantity(), detail.getCheckQuantity());
220   - if (differenceQuantity.compareTo(BigDecimal.ZERO) == 0) {
221   - continue;
222   - }
223   - WmsInventoryChangeReqDTO.Item item = BeanUtils.toBean(detail, WmsInventoryChangeReqDTO.Item.class)
224   - .setQuantity(differenceQuantity);
225   - items.add(item);
226   - }
227   - if (CollUtil.isEmpty(items)) {
228   - return;
229   - }
230   - inventoryService.changeInventory(new WmsInventoryChangeReqDTO()
  209 + private WmsInventoryCheckReqDTO buildCheckInventoryReqDTO(WmsCheckOrderDO order,
  210 + List<WmsCheckOrderDetailDO> details) {
  211 + return new WmsInventoryCheckReqDTO()
231 212 .setOrderId(order.getId()).setOrderNo(order.getNo())
232   - .setOrderType(WmsOrderTypeEnum.CHECK.getType()).setItems(items));
  213 + .setOrderType(WmsOrderTypeEnum.CHECK.getType())
  214 + .setItems(BeanUtils.toBean(details, WmsInventoryCheckReqDTO.Item.class));
233 215 }
234 216  
235 217 }
... ...
yudao-module-wms/src/test/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImplTest.java
... ... @@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.wms.dal.mysql.md.item.WmsItemMapper;
13 13 import cn.iocoder.yudao.module.wms.dal.mysql.md.item.WmsItemSkuMapper;
14 14 import cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum;
15 15 import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryChangeReqDTO;
  16 +import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryCheckReqDTO;
16 17 import cn.iocoder.yudao.module.wms.service.md.item.WmsItemService;
17 18 import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService;
18 19 import jakarta.annotation.Resource;
... ... @@ -32,6 +33,7 @@ import java.util.concurrent.Future;
32 33 import java.util.concurrent.TimeUnit;
33 34  
34 35 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  36 +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.CHECK_ORDER_INVENTORY_CHANGED;
35 37 import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.INVENTORY_QUANTITY_NOT_ENOUGH;
36 38 import static org.junit.jupiter.api.Assertions.assertEquals;
37 39 import static org.junit.jupiter.api.Assertions.assertTrue;
... ... @@ -325,6 +327,113 @@ public class WmsInventoryServiceImplTest extends BaseDbUnitTest {
325 327 }
326 328  
327 329 @Test
  330 + public void testCheckInventory_updateExistingInventory() {
  331 + // mock 数据
  332 + WmsItemDO item = createItem("ITEM-001", "红富士苹果");
  333 + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装");
  334 + WmsInventoryDO inventory = createInventory(sku.getId(), 100L, "10.00");
  335 + inventoryMapper.insert(inventory);
  336 + WmsInventoryCheckReqDTO reqDTO = createCheckReq(inventory.getId(), sku.getId(), 100L,
  337 + "10.00", "7.00");
  338 +
  339 + // 调用
  340 + inventoryService.checkInventory(reqDTO);
  341 +
  342 + // 断言:库存余额直接调整为实盘数量
  343 + WmsInventoryDO dbInventory = inventoryMapper.selectById(inventory.getId());
  344 + assertNotNull(dbInventory);
  345 + assertEquals(0, new BigDecimal("7.00").compareTo(dbInventory.getQuantity()));
  346 + // 断言:库存流水记录盘库差异
  347 + List<WmsInventoryHistoryDO> histories = inventoryHistoryMapper.selectList();
  348 + assertEquals(1, histories.size());
  349 + WmsInventoryHistoryDO history = histories.get(0);
  350 + assertEquals(sku.getId(), history.getSkuId());
  351 + assertEquals(100L, history.getWarehouseId());
  352 + assertEquals(0, new BigDecimal("10.00").compareTo(history.getBeforeQuantity()));
  353 + assertEquals(0, new BigDecimal("7.00").compareTo(history.getAfterQuantity()));
  354 + assertEquals(0, new BigDecimal("-3.00").compareTo(history.getQuantity()));
  355 + assertEquals(reqDTO.getOrderId(), history.getOrderId());
  356 + assertEquals(reqDTO.getOrderNo(), history.getOrderNo());
  357 + assertEquals(reqDTO.getOrderType(), history.getOrderType());
  358 + }
  359 +
  360 + @Test
  361 + public void testCheckInventory_existingInventoryChanged() {
  362 + // mock 数据
  363 + WmsItemDO item = createItem("ITEM-001", "红富士苹果");
  364 + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装");
  365 + WmsInventoryDO inventory = createInventory(sku.getId(), 100L, "12.00");
  366 + inventoryMapper.insert(inventory);
  367 + WmsInventoryCheckReqDTO reqDTO = createCheckReq(inventory.getId(), sku.getId(), 100L,
  368 + "10.00", "7.00");
  369 +
  370 + // 调用,并断言
  371 + assertServiceException(() -> inventoryService.checkInventory(reqDTO), CHECK_ORDER_INVENTORY_CHANGED);
  372 + WmsInventoryDO dbInventory = inventoryMapper.selectById(inventory.getId());
  373 + assertNotNull(dbInventory);
  374 + assertEquals(0, new BigDecimal("12.00").compareTo(dbInventory.getQuantity()));
  375 + assertEquals(0L, inventoryHistoryMapper.selectCount());
  376 + }
  377 +
  378 + @Test
  379 + public void testCheckInventory_createNewInventory() {
  380 + // mock 数据
  381 + WmsItemDO item = createItem("ITEM-001", "红富士苹果");
  382 + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装");
  383 + WmsInventoryCheckReqDTO reqDTO = createCheckReq(null, sku.getId(), 100L,
  384 + "0.00", "5.00");
  385 +
  386 + // 调用
  387 + inventoryService.checkInventory(reqDTO);
  388 +
  389 + // 断言:新增库存直接落为实盘数量
  390 + WmsInventoryDO inventory = inventoryMapper.selectBySkuIdAndWarehouseId(sku.getId(), 100L);
  391 + assertNotNull(inventory);
  392 + assertEquals(0, new BigDecimal("5.00").compareTo(inventory.getQuantity()));
  393 + // 断言:库存流水记录从 0 到实盘数量
  394 + List<WmsInventoryHistoryDO> histories = inventoryHistoryMapper.selectList();
  395 + assertEquals(1, histories.size());
  396 + assertEquals(0, BigDecimal.ZERO.compareTo(histories.get(0).getBeforeQuantity()));
  397 + assertEquals(0, new BigDecimal("5.00").compareTo(histories.get(0).getAfterQuantity()));
  398 + assertEquals(0, new BigDecimal("5.00").compareTo(histories.get(0).getQuantity()));
  399 + }
  400 +
  401 + @Test
  402 + public void testCheckInventory_createNewZeroInventory() {
  403 + // mock 数据
  404 + WmsItemDO item = createItem("ITEM-001", "红富士苹果");
  405 + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装");
  406 + WmsInventoryCheckReqDTO reqDTO = createCheckReq(null, sku.getId(), 100L,
  407 + "0.00", "0.00");
  408 +
  409 + // 调用
  410 + inventoryService.checkInventory(reqDTO);
  411 +
  412 + // 断言:新增零库存商品也会生成库存余额行
  413 + WmsInventoryDO inventory = inventoryMapper.selectBySkuIdAndWarehouseId(sku.getId(), 100L);
  414 + assertNotNull(inventory);
  415 + assertEquals(0, BigDecimal.ZERO.compareTo(inventory.getQuantity()));
  416 + assertEquals(0L, inventoryHistoryMapper.selectCount());
  417 + }
  418 +
  419 + @Test
  420 + public void testCheckInventory_newInventoryAlreadyExists() {
  421 + // mock 数据
  422 + WmsItemDO item = createItem("ITEM-001", "红富士苹果");
  423 + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装");
  424 + inventoryMapper.insert(createInventory(sku.getId(), 100L, "3.00"));
  425 + WmsInventoryCheckReqDTO reqDTO = createCheckReq(null, sku.getId(), 100L,
  426 + "0.00", "5.00");
  427 +
  428 + // 调用,并断言
  429 + assertServiceException(() -> inventoryService.checkInventory(reqDTO), CHECK_ORDER_INVENTORY_CHANGED);
  430 + WmsInventoryDO inventory = inventoryMapper.selectBySkuIdAndWarehouseId(sku.getId(), 100L);
  431 + assertNotNull(inventory);
  432 + assertEquals(0, new BigDecimal("3.00").compareTo(inventory.getQuantity()));
  433 + assertEquals(0L, inventoryHistoryMapper.selectCount());
  434 + }
  435 +
  436 + @Test
328 437 public void testGetInventoryPage_filterByWarehouse() {
329 438 // mock 数据
330 439 WmsItemDO item = createItem("ITEM-001", "红富士苹果");
... ... @@ -487,4 +596,20 @@ public class WmsInventoryServiceImplTest extends BaseDbUnitTest {
487 596 .setRemark("测试入库")));
488 597 }
489 598  
  599 + private static WmsInventoryCheckReqDTO createCheckReq(Long inventoryId, Long skuId, Long warehouseId,
  600 + String quantity, String checkQuantity) {
  601 + return new WmsInventoryCheckReqDTO()
  602 + .setOrderId(2L)
  603 + .setOrderNo("PK202605120001")
  604 + .setOrderType(WmsOrderTypeEnum.CHECK.getType())
  605 + .setItems(Collections.singletonList(new WmsInventoryCheckReqDTO.Item()
  606 + .setInventoryId(inventoryId)
  607 + .setSkuId(skuId)
  608 + .setWarehouseId(warehouseId)
  609 + .setQuantity(new BigDecimal(quantity))
  610 + .setCheckQuantity(new BigDecimal(checkQuantity))
  611 + .setPrice(new BigDecimal("100.00"))
  612 + .setRemark("测试盘库")));
  613 + }
  614 +
490 615 }
... ...