Commit d8f9f0d02920483d359e97c6eace5ce8c6a36d6e
1 parent
10ef8074
feat(wms):调整 check 的实现
Showing
6 changed files
with
158 additions
and
25 deletions
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 | } | ... | ... |