diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/inventory/WmsInventoryDetailMapper.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/inventory/WmsInventoryDetailMapper.java index 51f55db..79399cf 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/inventory/WmsInventoryDetailMapper.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/inventory/WmsInventoryDetailMapper.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.wms.dal.mysql.inventory; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX; import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.detail.WmsInventoryDetailPageReqVO; import cn.iocoder.yudao.module.wms.dal.dataobject.inventory.WmsInventoryDetailDO; @@ -15,6 +16,8 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.util.Collection; +import java.util.List; /** * WMS 库存明细 Mapper @@ -73,4 +76,29 @@ public interface WmsInventoryDetailMapper extends BaseMapperX selectListByIdsForUpdate(Collection ids) { + return selectList(new LambdaQueryWrapperX() + .in(WmsInventoryDetailDO::getId, ids) + .orderByAsc(WmsInventoryDetailDO::getId) + .last("FOR UPDATE")); + } + + /** + * 增量更新库存明细剩余数量 + * + * @param id 库存明细编号 + * @param quantity 变更数量 + * @return 更新行数 + */ + @SuppressWarnings("UnusedReturnValue") + default int updateRemainQuantityIncr(Long id, BigDecimal quantity) { + return update(null, new LambdaUpdateWrapper() + .eq(WmsInventoryDetailDO::getId, id) + .setSql("remain_quantity = remain_quantity + (" + quantity + ")")); + } + } diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/check/WmsCheckOrderDetailMapper.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/check/WmsCheckOrderDetailMapper.java index 25a794d..b597c9c 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/check/WmsCheckOrderDetailMapper.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/check/WmsCheckOrderDetailMapper.java @@ -31,4 +31,12 @@ public interface WmsCheckOrderDetailMapper extends BaseMapperX { return selectOne(WmsCheckOrderDO::getNo, no); } + default Long selectCountByWarehouseId(Long warehouseId) { + return selectCount(WmsCheckOrderDO::getWarehouseId, warehouseId); + } + + default Long selectCountByAreaId(Long areaId) { + return selectCount(WmsCheckOrderDO::getAreaId, areaId); + } + } diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/movement/WmsMovementOrderDetailMapper.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/movement/WmsMovementOrderDetailMapper.java index 7d1407b..7c5e901 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/movement/WmsMovementOrderDetailMapper.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/movement/WmsMovementOrderDetailMapper.java @@ -31,4 +31,14 @@ public interface WmsMovementOrderDetailMapper extends BaseMapperX() + .and(query -> query.eq(WmsMovementOrderDetailDO::getSourceAreaId, areaId) + .or().eq(WmsMovementOrderDetailDO::getTargetAreaId, areaId))); + } + } diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/movement/WmsMovementOrderMapper.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/movement/WmsMovementOrderMapper.java index ebdc3a5..78b3c74 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/movement/WmsMovementOrderMapper.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/movement/WmsMovementOrderMapper.java @@ -37,4 +37,16 @@ public interface WmsMovementOrderMapper extends BaseMapperX return selectOne(WmsMovementOrderDO::getNo, no); } + default Long selectCountByWarehouseId(Long warehouseId) { + return selectCount(new LambdaQueryWrapperX() + .and(query -> query.eq(WmsMovementOrderDO::getSourceWarehouseId, warehouseId) + .or().eq(WmsMovementOrderDO::getTargetWarehouseId, warehouseId))); + } + + default Long selectCountByAreaId(Long areaId) { + return selectCount(new LambdaQueryWrapperX() + .and(query -> query.eq(WmsMovementOrderDO::getSourceAreaId, areaId) + .or().eq(WmsMovementOrderDO::getTargetAreaId, areaId))); + } + } diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/receipt/WmsReceiptOrderDetailMapper.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/receipt/WmsReceiptOrderDetailMapper.java index aac5901..4a36af3 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/receipt/WmsReceiptOrderDetailMapper.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/receipt/WmsReceiptOrderDetailMapper.java @@ -31,4 +31,12 @@ public interface WmsReceiptOrderDetailMapper extends BaseMapperX { return selectOne(WmsReceiptOrderDO::getNo, no); } + default Long selectCountByMerchantId(Long merchantId) { + return selectCount(WmsReceiptOrderDO::getMerchantId, merchantId); + } + + default Long selectCountByWarehouseId(Long warehouseId) { + return selectCount(WmsReceiptOrderDO::getWarehouseId, warehouseId); + } + + default Long selectCountByAreaId(Long areaId) { + return selectCount(WmsReceiptOrderDO::getAreaId, areaId); + } + } diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/shipment/WmsShipmentOrderDetailMapper.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/shipment/WmsShipmentOrderDetailMapper.java index c03e35b..f8485c9 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/shipment/WmsShipmentOrderDetailMapper.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/shipment/WmsShipmentOrderDetailMapper.java @@ -31,4 +31,12 @@ public interface WmsShipmentOrderDetailMapper extends BaseMapperX return selectOne(WmsShipmentOrderDO::getNo, no); } + default Long selectCountByMerchantId(Long merchantId) { + return selectCount(WmsShipmentOrderDO::getMerchantId, merchantId); + } + + default Long selectCountByWarehouseId(Long warehouseId) { + return selectCount(WmsShipmentOrderDO::getWarehouseId, warehouseId); + } + + default Long selectCountByAreaId(Long areaId) { + return selectCount(WmsShipmentOrderDO::getAreaId, areaId); + } + } diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/enums/DictTypeConstants.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/enums/DictTypeConstants.java index e8f2756..973f8cb 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/enums/DictTypeConstants.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/enums/DictTypeConstants.java @@ -12,7 +12,5 @@ public interface DictTypeConstants { String RECEIPT_ORDER_TYPE = "wms_receipt_order_type"; String SHIPMENT_ORDER_TYPE = "wms_shipment_order_type"; String INVENTORY_HISTORY_TYPE = "wms_inventory_history_type"; - String MOVEMENT_STATUS = "wms_movement_status"; - String CHECK_STATUS = "wms_check_status"; } diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/enums/ErrorCodeConstants.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/enums/ErrorCodeConstants.java index ad6a87d..9673986 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/enums/ErrorCodeConstants.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/enums/ErrorCodeConstants.java @@ -17,12 +17,14 @@ public interface ErrorCodeConstants { ErrorCode WAREHOUSE_NAME_DUPLICATE = new ErrorCode(1_060_100_001, "仓库名称重复"); ErrorCode WAREHOUSE_CODE_DUPLICATE = new ErrorCode(1_060_100_002, "仓库编号重复"); ErrorCode WAREHOUSE_HAS_AREA = new ErrorCode(1_060_100_003, "删除失败!请先删除该仓库下的库区!"); + ErrorCode WAREHOUSE_HAS_ORDER = new ErrorCode(1_060_100_004, "删除失败!仓库已被{}使用!"); // ========== WMS 基础数据-库区 1-060-101-000 ========== ErrorCode WAREHOUSE_AREA_NOT_EXISTS = new ErrorCode(1_060_101_000, "库区不存在"); ErrorCode WAREHOUSE_AREA_NAME_DUPLICATE = new ErrorCode(1_060_101_001, "库区名称重复"); ErrorCode WAREHOUSE_AREA_CODE_DUPLICATE = new ErrorCode(1_060_101_002, "库区编号重复"); ErrorCode WAREHOUSE_AREA_NOT_MATCH_WAREHOUSE = new ErrorCode(1_060_101_003, "库区不属于仓库"); + ErrorCode WAREHOUSE_AREA_HAS_ORDER = new ErrorCode(1_060_101_004, "删除失败!库区已被{}使用!"); // ========== WMS 基础数据-商品分类 1-060-102-000 ========== ErrorCode ITEM_CATEGORY_NOT_EXISTS = new ErrorCode(1_060_102_000, "商品分类不存在"); @@ -44,11 +46,13 @@ public interface ErrorCodeConstants { ErrorCode ITEM_SKU_NAME_DUPLICATE = new ErrorCode(1_060_104_003, "商品规格名称【{}】重复"); ErrorCode ITEM_SKU_NOT_EXISTS = new ErrorCode(1_060_104_004, "商品规格不存在"); ErrorCode ITEM_SKU_HAS_INVENTORY = new ErrorCode(1_060_104_005, "删除失败!商品规格【{}】已被库存业务使用!"); + ErrorCode ITEM_SKU_HAS_ORDER = new ErrorCode(1_060_104_006, "删除失败!商品规格【{}】已被{}使用!"); // ========== WMS 基础数据-往来企业 1-060-105-000 ========== ErrorCode MERCHANT_NOT_EXISTS = new ErrorCode(1_060_105_000, "往来企业不存在"); ErrorCode MERCHANT_NOT_SUPPLIER = new ErrorCode(1_060_105_001, "往来企业必须是供应商或客户/供应商类型"); ErrorCode MERCHANT_NOT_CUSTOMER = new ErrorCode(1_060_105_002, "往来企业必须是客户或客户/供应商类型"); + ErrorCode MERCHANT_HAS_ORDER = new ErrorCode(1_060_105_003, "删除失败!往来企业已被{}使用!"); // ========== WMS 入库单 1-060-200-000 ========== ErrorCode RECEIPT_ORDER_NOT_EXISTS = new ErrorCode(1_060_200_000, "入库单不存在"); @@ -68,6 +72,25 @@ public interface ErrorCodeConstants { ErrorCode SHIPMENT_ORDER_STATUS_NOT_DELETABLE = new ErrorCode(1_060_201_005, "出库单状态不是草稿或已作废,不能删除"); ErrorCode SHIPMENT_ORDER_DETAIL_NOT_EXISTS = new ErrorCode(1_060_201_007, "出库单明细不存在"); + // ========== WMS 移库单 1-060-202-000 ========== + ErrorCode MOVEMENT_ORDER_NOT_EXISTS = new ErrorCode(1_060_202_000, "移库单不存在"); + ErrorCode MOVEMENT_ORDER_NO_DUPLICATE = new ErrorCode(1_060_202_001, "移库单号重复"); + ErrorCode MOVEMENT_ORDER_STATUS_NOT_PREPARE = new ErrorCode(1_060_202_002, "移库单状态不是草稿,不能操作"); + ErrorCode MOVEMENT_ORDER_DETAIL_REQUIRED = new ErrorCode(1_060_202_003, "移库单至少包含一条明细"); + ErrorCode MOVEMENT_ORDER_AREA_REQUIRED = new ErrorCode(1_060_202_004, "库区模式下,移库单库区不能为空"); + ErrorCode MOVEMENT_ORDER_STATUS_NOT_DELETABLE = new ErrorCode(1_060_202_005, "移库单状态不是草稿或已作废,不能删除"); + ErrorCode MOVEMENT_ORDER_DETAIL_NOT_EXISTS = new ErrorCode(1_060_202_006, "移库单明细不存在"); + ErrorCode MOVEMENT_ORDER_WAREHOUSE_AREA_SAME = new ErrorCode(1_060_202_007, "来源仓库库区和目标仓库库区不能相同"); + + // ========== WMS 盘库单 1-060-203-000 ========== + ErrorCode CHECK_ORDER_NOT_EXISTS = new ErrorCode(1_060_203_000, "盘库单不存在"); + ErrorCode CHECK_ORDER_NO_DUPLICATE = new ErrorCode(1_060_203_001, "盘库单号重复"); + ErrorCode CHECK_ORDER_STATUS_NOT_PREPARE = new ErrorCode(1_060_203_002, "盘库单状态不是草稿,不能操作"); + ErrorCode CHECK_ORDER_DETAIL_REQUIRED = new ErrorCode(1_060_203_003, "盘库单至少包含一条明细"); + ErrorCode CHECK_ORDER_AREA_REQUIRED = new ErrorCode(1_060_203_004, "库区模式下,盘库单库区不能为空"); + ErrorCode CHECK_ORDER_STATUS_NOT_DELETABLE = new ErrorCode(1_060_203_005, "盘库单状态不是草稿或已作废,不能删除"); + ErrorCode CHECK_ORDER_DETAIL_NOT_EXISTS = new ErrorCode(1_060_203_006, "盘库单明细不存在"); + // ========== WMS 库存 1-060-300-000 ========== ErrorCode INVENTORY_QUANTITY_NOT_ENOUGH = new ErrorCode(1_060_300_000, "库存不足,当前库存:{},变更数量:{}"); ErrorCode INVENTORY_DETAIL_NOT_EXISTS = new ErrorCode(1_060_300_001, "库存明细不存在"); diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsOrderTypeConstants.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsOrderTypeConstants.java index 52a89a5..5738ba5 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsOrderTypeConstants.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsOrderTypeConstants.java @@ -12,6 +12,13 @@ public final class WmsOrderTypeConstants { private WmsOrderTypeConstants() {} + // ========== 单据名称 ========== + + public static final String ORDER_NAME_RECEIPT = "入库单"; + public static final String ORDER_NAME_SHIPMENT = "出库单"; + public static final String ORDER_NAME_MOVEMENT = "移库单"; + public static final String ORDER_NAME_CHECK = "盘库单"; + // ========== 入库单类型 [100, 200) ========== public static final int RECEIPT_PRODUCTION = 100; // 生产入库:WmsReceiptOrderDO diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryDetailService.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryDetailService.java index e9cf81e..65b8e03 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryDetailService.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryDetailService.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.wms.service.inventory; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.detail.WmsInventoryDetailPageReqVO; import cn.iocoder.yudao.module.wms.dal.dataobject.inventory.WmsInventoryDetailDO; +import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryChangeReqDTO; import java.util.List; @@ -28,4 +29,11 @@ public interface WmsInventoryDetailService { */ void createInventoryDetailList(List list); + /** + * 扣减库存明细剩余数量 + * + * @param items 库存变更明细列表 + */ + void decreaseInventoryDetailList(List items); + } diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryDetailServiceImpl.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryDetailServiceImpl.java index 3eb77fd..6ee6d21 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryDetailServiceImpl.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryDetailServiceImpl.java @@ -1,16 +1,28 @@ package cn.iocoder.yudao.module.wms.service.inventory; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.detail.WmsInventoryDetailPageReqVO; import cn.iocoder.yudao.module.wms.dal.dataobject.inventory.WmsInventoryDetailDO; import cn.iocoder.yudao.module.wms.dal.mysql.inventory.WmsInventoryDetailMapper; +import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryChangeReqDTO; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; +import java.math.BigDecimal; +import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.INVENTORY_DETAIL_NOT_EXISTS; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.INVENTORY_DETAIL_REMAIN_QUANTITY_NOT_ENOUGH; /** * WMS 库存明细 Service 实现类 @@ -38,4 +50,47 @@ public class WmsInventoryDetailServiceImpl implements WmsInventoryDetailService inventoryDetailMapper.insertBatch(list); } + @Override + @Transactional(rollbackFor = Exception.class) + public void decreaseInventoryDetailList(List items) { + if (CollUtil.isEmpty(items)) { + return; + } + if (items.stream().anyMatch(item -> item.getInventoryDetailId() == null)) { + throw exception(INVENTORY_DETAIL_NOT_EXISTS); + } + List sortedItems = items.stream() + .sorted(Comparator.comparing(WmsInventoryChangeReqDTO.Item::getInventoryDetailId)) + .toList(); + + // 1. 按库存明细编号固定顺序加锁,避免多明细并发扣减时交叉拿锁 + Map detailMap = convertMap(inventoryDetailMapper.selectListByIdsForUpdate( + convertSet(sortedItems, WmsInventoryChangeReqDTO.Item::getInventoryDetailId)), + WmsInventoryDetailDO::getId); + + // 2. 校验库存明细归属,并合并相同库存明细的扣减数量 + Map decreaseQuantityMap = new LinkedHashMap<>(); + for (WmsInventoryChangeReqDTO.Item item : sortedItems) { + WmsInventoryDetailDO detail = detailMap.get(item.getInventoryDetailId()); + if (detail == null + || ObjectUtil.notEqual(detail.getSkuId(), item.getSkuId()) + || ObjectUtil.notEqual(detail.getWarehouseId(), item.getWarehouseId()) + || ObjectUtil.notEqual(detail.getAreaId(), item.getAreaId())) { + throw exception(INVENTORY_DETAIL_NOT_EXISTS); + } + decreaseQuantityMap.merge(detail.getId(), item.getQuantity().abs(), BigDecimal::add); + } + + // 3. 校验并扣减库存明细剩余数量 + for (Map.Entry entry : decreaseQuantityMap.entrySet()) { + WmsInventoryDetailDO detail = detailMap.get(entry.getKey()); + BigDecimal decreaseQuantity = entry.getValue(); + if (detail.getRemainQuantity().compareTo(decreaseQuantity) < 0) { + throw exception(INVENTORY_DETAIL_REMAIN_QUANTITY_NOT_ENOUGH, + detail.getRemainQuantity(), decreaseQuantity); + } + inventoryDetailMapper.updateRemainQuantityIncr(detail.getId(), decreaseQuantity.negate()); + } + } + } diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImpl.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImpl.java index 8cf79e3..81f9a3f 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImpl.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImpl.java @@ -19,7 +19,13 @@ import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Comparator; +import java.util.IdentityHashMap; import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; /** * WMS 库存 Service 实现类 @@ -61,44 +67,72 @@ public class WmsInventoryServiceImpl implements WmsInventoryService { return; } - // 1. 变更库存余额,并构建库存流水与批次库存明细 + // 1. 按固定顺序变更库存余额,避免多 SKU/仓库并发操作时交叉拿锁 + Map resultMap = changeInventoryList(reqDTO.getItems()); + + // 2. 构建库存流水与批次库存明细 boolean batchEnabled = wmsProperties.isBatchEnabled(); List histories = new ArrayList<>(reqDTO.getItems().size()); List details = batchEnabled ? new ArrayList<>(reqDTO.getItems().size()) : List.of(); + List decreaseItems = batchEnabled ? new ArrayList<>(reqDTO.getItems().size()) : List.of(); for (WmsInventoryChangeReqDTO.Item item : reqDTO.getItems()) { - // 1.1 变更库存余额 - Tuple result = changeInventory0(item); - // 1.2 构建库存流水与批次库存明细 - histories.add(buildInventoryHistory(reqDTO, item, result)); + histories.add(buildInventoryHistory(reqDTO, item, resultMap.get(item))); if (batchEnabled) { - details.add(buildInventoryDetail(reqDTO, item)); + if (isIncrease(item)) { + details.add(buildInventoryDetail(reqDTO, item)); + } else { + decreaseItems.add(item); + } } } - // 2.1 批量写入库存流水 - inventoryHistoryService.createInventoryHistoryList(histories); + // 3. 按固定顺序变更批次库存明细 if (batchEnabled) { - // 2.2 批量写入批次库存明细 + inventoryDetailService.decreaseInventoryDetailList(decreaseItems); inventoryDetailService.createInventoryDetailList(details); } + + // 4. 批量写入库存流水 + inventoryHistoryService.createInventoryHistoryList(histories); } /** - * 变更库存余额 + * 批量变更库存余额 * - * @param item 库存变更明细 - * @return 变更前数量、变更后数量 + * 按 SKU、仓库、库区固定顺序拿锁;相同库存余额只更新一次,避免同一批操作内反复更新。 + * + * @param items 库存变更明细列表 + * @return 每条变更明细对应的变更前数量、变更后数量 */ - private Tuple changeInventory0(WmsInventoryChangeReqDTO.Item item) { - // 1. 获取或创建库存余额,使用 FOR UPDATE 锁定行 - WmsInventoryDO inventory = getOrCreateInventory(item); - // 2. 变更库存余额 - BigDecimal beforeQuantity = inventory.getQuantity(); - BigDecimal afterQuantity = beforeQuantity.add(item.getQuantity()); - inventoryMapper.updateQuantity(inventory.getId(), item.getQuantity()); - return new Tuple(beforeQuantity, afterQuantity); - } + private Map changeInventoryList(List items) { + List sortedItems = sortByInventoryKey(items); + Map resultMap = new IdentityHashMap<>(items.size()); + for (int i = 0; i < sortedItems.size(); ) { + WmsInventoryChangeReqDTO.Item firstItem = sortedItems.get(i); + WmsInventoryDO inventory = getOrCreateInventory(firstItem); + BigDecimal currentQuantity = inventory.getQuantity(); + BigDecimal totalChangeQuantity = BigDecimal.ZERO; + + int j = i; + while (j < sortedItems.size() && isSameInventoryKey(firstItem, sortedItems.get(j))) { + WmsInventoryChangeReqDTO.Item item = sortedItems.get(j); + BigDecimal beforeQuantity = currentQuantity; + currentQuantity = currentQuantity.add(item.getQuantity()); + if (currentQuantity.compareTo(BigDecimal.ZERO) < 0) { + throw exception(INVENTORY_QUANTITY_NOT_ENOUGH, beforeQuantity, item.getQuantity()); + } + resultMap.put(item, new Tuple(beforeQuantity, currentQuantity)); + totalChangeQuantity = totalChangeQuantity.add(item.getQuantity()); + j++; + } + if (totalChangeQuantity.compareTo(BigDecimal.ZERO) != 0) { + inventoryMapper.updateQuantity(inventory.getId(), totalChangeQuantity); + } + i = j; + } + return resultMap; + } private WmsInventoryDO getOrCreateInventory(WmsInventoryChangeReqDTO.Item item) { // 1. 查询库存余额,使用 FOR UPDATE 锁定行 @@ -147,4 +181,23 @@ public class WmsInventoryServiceImpl implements WmsInventoryService { .setOrderId(reqDTO.getOrderId()).setOrderNo(reqDTO.getOrderNo()).setOrderType(reqDTO.getOrderType()); } + private boolean isIncrease(WmsInventoryChangeReqDTO.Item item) { + return item.getQuantity().compareTo(BigDecimal.ZERO) > 0; + } + + private static List sortByInventoryKey(List items) { + return items.stream() + .sorted(Comparator.comparing(WmsInventoryChangeReqDTO.Item::getSkuId) + .thenComparing(WmsInventoryChangeReqDTO.Item::getWarehouseId) + .thenComparing(WmsInventoryChangeReqDTO.Item::getAreaId)) + .toList(); + } + + private static boolean isSameInventoryKey(WmsInventoryChangeReqDTO.Item item1, + WmsInventoryChangeReqDTO.Item item2) { + return item1.getSkuId().equals(item2.getSkuId()) + && item1.getWarehouseId().equals(item2.getWarehouseId()) + && item1.getAreaId().equals(item2.getAreaId()); + } + } diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemSkuServiceImpl.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemSkuServiceImpl.java index 827449f..8a7fb49 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemSkuServiceImpl.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemSkuServiceImpl.java @@ -8,6 +8,10 @@ import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku.WmsItemSkuSav import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; import cn.iocoder.yudao.module.wms.dal.mysql.md.item.WmsItemSkuMapper; import cn.iocoder.yudao.module.wms.service.inventory.WmsInventoryService; +import cn.iocoder.yudao.module.wms.service.order.check.WmsCheckOrderDetailService; +import cn.iocoder.yudao.module.wms.service.order.movement.WmsMovementOrderDetailService; +import cn.iocoder.yudao.module.wms.service.order.receipt.WmsReceiptOrderDetailService; +import cn.iocoder.yudao.module.wms.service.order.shipment.WmsShipmentOrderDetailService; import cn.iocoder.yudao.module.wms.util.WmsUtils; import jakarta.annotation.Resource; import org.springframework.context.annotation.Lazy; @@ -23,6 +27,7 @@ import java.util.Set; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeConstants.*; /** * WMS 商品 SKU Service 实现类 @@ -38,6 +43,18 @@ public class WmsItemSkuServiceImpl implements WmsItemSkuService { @Resource @Lazy // 延迟加载,避免循环依赖 private WmsInventoryService inventoryService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsReceiptOrderDetailService receiptOrderDetailService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsShipmentOrderDetailService shipmentOrderDetailService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsMovementOrderDetailService movementOrderDetailService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsCheckOrderDetailService checkOrderDetailService; @Override @Transactional(rollbackFor = Exception.class) @@ -146,12 +163,28 @@ public class WmsItemSkuServiceImpl implements WmsItemSkuService { return; } for (WmsItemSkuDO sku : skus) { + validateItemSkuOrderUnused(sku); if (inventoryService.getInventoryCountBySkuId(sku.getId()) > 0) { throw exception(ITEM_SKU_HAS_INVENTORY, sku.getName()); } } } + private void validateItemSkuOrderUnused(WmsItemSkuDO sku) { + if (receiptOrderDetailService.getReceiptOrderDetailCountBySkuId(sku.getId()) > 0) { + throw exception(ITEM_SKU_HAS_ORDER, sku.getName(), ORDER_NAME_RECEIPT); + } + if (shipmentOrderDetailService.getShipmentOrderDetailCountBySkuId(sku.getId()) > 0) { + throw exception(ITEM_SKU_HAS_ORDER, sku.getName(), ORDER_NAME_SHIPMENT); + } + if (movementOrderDetailService.getMovementOrderDetailCountBySkuId(sku.getId()) > 0) { + throw exception(ITEM_SKU_HAS_ORDER, sku.getName(), ORDER_NAME_MOVEMENT); + } + if (checkOrderDetailService.getCheckOrderDetailCountBySkuId(sku.getId()) > 0) { + throw exception(ITEM_SKU_HAS_ORDER, sku.getName(), ORDER_NAME_CHECK); + } + } + /** * 填充空的条形码 * diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/merchant/WmsMerchantServiceImpl.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/merchant/WmsMerchantServiceImpl.java index cff2439..23552fb 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/merchant/WmsMerchantServiceImpl.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/merchant/WmsMerchantServiceImpl.java @@ -10,7 +10,10 @@ import cn.iocoder.yudao.module.wms.controller.admin.md.merchant.vo.WmsMerchantSa import cn.iocoder.yudao.module.wms.dal.dataobject.md.merchant.WmsMerchantDO; import cn.iocoder.yudao.module.wms.dal.mysql.md.merchant.WmsMerchantMapper; import cn.iocoder.yudao.module.wms.enums.md.WmsMerchantTypeEnum; +import cn.iocoder.yudao.module.wms.service.order.receipt.WmsReceiptOrderService; +import cn.iocoder.yudao.module.wms.service.order.shipment.WmsShipmentOrderService; import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -19,6 +22,7 @@ import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeConstants.*; /** * WMS 往来企业 Service 实现类 @@ -31,6 +35,12 @@ public class WmsMerchantServiceImpl implements WmsMerchantService { @Resource private WmsMerchantMapper merchantMapper; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsReceiptOrderService receiptOrderService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsShipmentOrderService shipmentOrderService; @Override public Long createMerchant(WmsMerchantSaveReqVO createReqVO) { @@ -53,12 +63,22 @@ public class WmsMerchantServiceImpl implements WmsMerchantService { public void deleteMerchant(Long id) { // 校验存在 validateMerchantExists(id); - // TODO 入库/出库单实现后,校验往来企业不存在业务关联后再删除 + // 校验未被单据使用 + validateMerchantUnused(id); // 删除 merchantMapper.deleteById(id); } + private void validateMerchantUnused(Long id) { + if (receiptOrderService.getReceiptOrderCountByMerchantId(id) > 0) { + throw exception(MERCHANT_HAS_ORDER, ORDER_NAME_RECEIPT); + } + if (shipmentOrderService.getShipmentOrderCountByMerchantId(id) > 0) { + throw exception(MERCHANT_HAS_ORDER, ORDER_NAME_SHIPMENT); + } + } + @Override public WmsMerchantDO validateMerchantExists(Long id) { WmsMerchantDO merchant = merchantMapper.selectById(id); diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseAreaServiceImpl.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseAreaServiceImpl.java index 1a0f00b..7bdaee2 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseAreaServiceImpl.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseAreaServiceImpl.java @@ -11,7 +11,16 @@ import cn.iocoder.yudao.module.wms.controller.admin.md.warehouse.vo.area.WmsWare import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseAreaDO; import cn.iocoder.yudao.module.wms.dal.mysql.md.warehouse.WmsWarehouseAreaMapper; import cn.iocoder.yudao.module.wms.framework.config.WmsProperties; +import cn.iocoder.yudao.module.wms.service.order.check.WmsCheckOrderDetailService; +import cn.iocoder.yudao.module.wms.service.order.check.WmsCheckOrderService; +import cn.iocoder.yudao.module.wms.service.order.movement.WmsMovementOrderDetailService; +import cn.iocoder.yudao.module.wms.service.order.movement.WmsMovementOrderService; +import cn.iocoder.yudao.module.wms.service.order.receipt.WmsReceiptOrderDetailService; +import cn.iocoder.yudao.module.wms.service.order.receipt.WmsReceiptOrderService; +import cn.iocoder.yudao.module.wms.service.order.shipment.WmsShipmentOrderDetailService; +import cn.iocoder.yudao.module.wms.service.order.shipment.WmsShipmentOrderService; import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -21,6 +30,7 @@ import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeConstants.*; /** * WMS 库区 Service 实现类 @@ -37,6 +47,30 @@ public class WmsWarehouseAreaServiceImpl implements WmsWarehouseAreaService { private WmsWarehouseService warehouseService; @Resource private WmsProperties wmsProperties; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsReceiptOrderService receiptOrderService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsReceiptOrderDetailService receiptOrderDetailService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsShipmentOrderService shipmentOrderService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsShipmentOrderDetailService shipmentOrderDetailService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsMovementOrderService movementOrderService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsMovementOrderDetailService movementOrderDetailService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsCheckOrderService checkOrderService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsCheckOrderDetailService checkOrderDetailService; @Override public Long createWarehouseArea(WmsWarehouseAreaSaveReqVO createReqVO) { @@ -98,11 +132,32 @@ public class WmsWarehouseAreaServiceImpl implements WmsWarehouseAreaService { validateAreaEnabled(); // 校验存在 validateWarehouseAreaExists(id); + // 校验未被单据使用 + validateWarehouseAreaUnused(id); // 删除 warehouseAreaMapper.deleteById(id); } + private void validateWarehouseAreaUnused(Long id) { + if (receiptOrderService.getReceiptOrderCountByAreaId(id) > 0 + || receiptOrderDetailService.getReceiptOrderDetailCountByAreaId(id) > 0) { + throw exception(WAREHOUSE_AREA_HAS_ORDER, ORDER_NAME_RECEIPT); + } + if (shipmentOrderService.getShipmentOrderCountByAreaId(id) > 0 + || shipmentOrderDetailService.getShipmentOrderDetailCountByAreaId(id) > 0) { + throw exception(WAREHOUSE_AREA_HAS_ORDER, ORDER_NAME_SHIPMENT); + } + if (movementOrderService.getMovementOrderCountByAreaId(id) > 0 + || movementOrderDetailService.getMovementOrderDetailCountByAreaId(id) > 0) { + throw exception(WAREHOUSE_AREA_HAS_ORDER, ORDER_NAME_MOVEMENT); + } + if (checkOrderService.getCheckOrderCountByAreaId(id) > 0 + || checkOrderDetailService.getCheckOrderDetailCountByAreaId(id) > 0) { + throw exception(WAREHOUSE_AREA_HAS_ORDER, ORDER_NAME_CHECK); + } + } + @Override public WmsWarehouseAreaDO validateWarehouseAreaExists(Long id) { validateAreaEnabled(); diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseServiceImpl.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseServiceImpl.java index 33d1b9d..7c480c8 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseServiceImpl.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseServiceImpl.java @@ -11,7 +11,12 @@ import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; import cn.iocoder.yudao.module.wms.dal.mysql.md.warehouse.WmsWarehouseAreaMapper; import cn.iocoder.yudao.module.wms.dal.mysql.md.warehouse.WmsWarehouseMapper; import cn.iocoder.yudao.module.wms.framework.config.WmsProperties; +import cn.iocoder.yudao.module.wms.service.order.check.WmsCheckOrderService; +import cn.iocoder.yudao.module.wms.service.order.movement.WmsMovementOrderService; +import cn.iocoder.yudao.module.wms.service.order.receipt.WmsReceiptOrderService; +import cn.iocoder.yudao.module.wms.service.order.shipment.WmsShipmentOrderService; import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -21,6 +26,7 @@ import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeConstants.*; /** * WMS 仓库 Service 实现类 @@ -37,6 +43,18 @@ public class WmsWarehouseServiceImpl implements WmsWarehouseService { private WmsWarehouseAreaMapper warehouseAreaMapper; @Resource private WmsProperties wmsProperties; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsReceiptOrderService receiptOrderService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsShipmentOrderService shipmentOrderService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsMovementOrderService movementOrderService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsCheckOrderService checkOrderService; @Override public Long createWarehouse(WmsWarehouseSaveReqVO createReqVO) { @@ -93,6 +111,8 @@ public class WmsWarehouseServiceImpl implements WmsWarehouseService { public void deleteWarehouse(Long id) { // 校验存在 validateWarehouseExists(id); + // 校验未被单据使用 + validateWarehouseUnused(id); // 库区模式下,存在库区时不允许删除仓库 if (wmsProperties.isAreaEnabled() && warehouseAreaMapper.selectCountByWarehouseId(id) > 0) { throw exception(WAREHOUSE_HAS_AREA); @@ -102,6 +122,21 @@ public class WmsWarehouseServiceImpl implements WmsWarehouseService { warehouseMapper.deleteById(id); } + private void validateWarehouseUnused(Long id) { + if (receiptOrderService.getReceiptOrderCountByWarehouseId(id) > 0) { + throw exception(WAREHOUSE_HAS_ORDER, ORDER_NAME_RECEIPT); + } + if (shipmentOrderService.getShipmentOrderCountByWarehouseId(id) > 0) { + throw exception(WAREHOUSE_HAS_ORDER, ORDER_NAME_SHIPMENT); + } + if (movementOrderService.getMovementOrderCountByWarehouseId(id) > 0) { + throw exception(WAREHOUSE_HAS_ORDER, ORDER_NAME_MOVEMENT); + } + if (checkOrderService.getCheckOrderCountByWarehouseId(id) > 0) { + throw exception(WAREHOUSE_HAS_ORDER, ORDER_NAME_CHECK); + } + } + @Override public WmsWarehouseDO validateWarehouseExists(Long id) { WmsWarehouseDO warehouse = warehouseMapper.selectById(id); diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailService.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailService.java index 8f9b23b..5d9c67c 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailService.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailService.java @@ -60,4 +60,20 @@ public interface WmsCheckOrderDetailService { */ List validateCheckOrderDetailListExists(Long orderId); + /** + * 获得指定 SKU 的盘库单明细数量 + * + * @param skuId SKU 编号 + * @return 盘库单明细数量 + */ + long getCheckOrderDetailCountBySkuId(Long skuId); + + /** + * 获得指定库区的盘库单明细数量 + * + * @param areaId 库区编号 + * @return 盘库单明细数量 + */ + long getCheckOrderDetailCountByAreaId(Long areaId); + } diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailServiceImpl.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailServiceImpl.java index fd41711..76ecbd6 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailServiceImpl.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailServiceImpl.java @@ -111,6 +111,16 @@ public class WmsCheckOrderDetailServiceImpl implements WmsCheckOrderDetailServic return details; } + @Override + public long getCheckOrderDetailCountBySkuId(Long skuId) { + return checkOrderDetailMapper.selectCountBySkuId(skuId); + } + + @Override + public long getCheckOrderDetailCountByAreaId(Long areaId) { + return checkOrderDetailMapper.selectCountByAreaId(areaId); + } + private List buildCheckOrderDetailList(WmsCheckOrderSaveReqVO reqVO) { if (CollUtil.isEmpty(reqVO.getDetails())) { return ListUtil.of(); diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderService.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderService.java index 766c47b..c213f6e 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderService.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderService.java @@ -65,4 +65,20 @@ public interface WmsCheckOrderService { */ PageResult getCheckOrderPage(WmsCheckOrderPageReqVO pageReqVO); + /** + * 获得指定仓库的盘库单数量 + * + * @param warehouseId 仓库编号 + * @return 盘库单数量 + */ + long getCheckOrderCountByWarehouseId(Long warehouseId); + + /** + * 获得指定库区的盘库单数量 + * + * @param areaId 库区编号 + * @return 盘库单数量 + */ + long getCheckOrderCountByAreaId(Long areaId); + } diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderServiceImpl.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderServiceImpl.java index 7b66992..f04ea37 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderServiceImpl.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderServiceImpl.java @@ -129,6 +129,16 @@ public class WmsCheckOrderServiceImpl implements WmsCheckOrderService { return checkOrderMapper.selectPage(pageReqVO); } + @Override + public long getCheckOrderCountByWarehouseId(Long warehouseId) { + return checkOrderMapper.selectCountByWarehouseId(warehouseId); + } + + @Override + public long getCheckOrderCountByAreaId(Long areaId) { + return checkOrderMapper.selectCountByAreaId(areaId); + } + private void validateCheckOrderSaveData(WmsCheckOrderSaveReqVO reqVO) { // 校验盘库单号唯一 validateCheckOrderNoUnique(reqVO.getId(), reqVO.getNo()); diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailService.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailService.java index e59f8ed..0be42ea 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailService.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailService.java @@ -60,4 +60,20 @@ public interface WmsMovementOrderDetailService { */ List validateMovementOrderDetailListExists(Long orderId); + /** + * 获得指定 SKU 的移库单明细数量 + * + * @param skuId SKU 编号 + * @return 移库单明细数量 + */ + long getMovementOrderDetailCountBySkuId(Long skuId); + + /** + * 获得指定库区的移库单明细数量 + * + * @param areaId 库区编号 + * @return 移库单明细数量 + */ + long getMovementOrderDetailCountByAreaId(Long areaId); + } diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailServiceImpl.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailServiceImpl.java index 5b44408..7fdbb48 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailServiceImpl.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailServiceImpl.java @@ -112,6 +112,16 @@ public class WmsMovementOrderDetailServiceImpl implements WmsMovementOrderDetail return details; } + @Override + public long getMovementOrderDetailCountBySkuId(Long skuId) { + return movementOrderDetailMapper.selectCountBySkuId(skuId); + } + + @Override + public long getMovementOrderDetailCountByAreaId(Long areaId) { + return movementOrderDetailMapper.selectCountByAreaId(areaId); + } + private List buildMovementOrderDetailList(WmsMovementOrderSaveReqVO reqVO) { if (CollUtil.isEmpty(reqVO.getDetails())) { return ListUtil.of(); diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderService.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderService.java index c3bba06..4747bb2 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderService.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderService.java @@ -65,4 +65,20 @@ public interface WmsMovementOrderService { */ PageResult getMovementOrderPage(WmsMovementOrderPageReqVO pageReqVO); + /** + * 获得指定仓库的移库单数量 + * + * @param warehouseId 仓库编号 + * @return 移库单数量 + */ + long getMovementOrderCountByWarehouseId(Long warehouseId); + + /** + * 获得指定库区的移库单数量 + * + * @param areaId 库区编号 + * @return 移库单数量 + */ + long getMovementOrderCountByAreaId(Long areaId); + } diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderServiceImpl.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderServiceImpl.java index 39367f0..a15ba51 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderServiceImpl.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderServiceImpl.java @@ -128,6 +128,16 @@ public class WmsMovementOrderServiceImpl implements WmsMovementOrderService { return movementOrderMapper.selectPage(pageReqVO); } + @Override + public long getMovementOrderCountByWarehouseId(Long warehouseId) { + return movementOrderMapper.selectCountByWarehouseId(warehouseId); + } + + @Override + public long getMovementOrderCountByAreaId(Long areaId) { + return movementOrderMapper.selectCountByAreaId(areaId); + } + private void validateMovementOrderSaveData(WmsMovementOrderSaveReqVO reqVO) { // 校验移库单号唯一 validateMovementOrderNoUnique(reqVO.getId(), reqVO.getNo()); diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailService.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailService.java index 03122cb..46f87e1 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailService.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailService.java @@ -60,4 +60,20 @@ public interface WmsReceiptOrderDetailService { */ List validateReceiptOrderDetailListExists(Long orderId); + /** + * 获得指定 SKU 的入库单明细数量 + * + * @param skuId SKU 编号 + * @return 入库单明细数量 + */ + long getReceiptOrderDetailCountBySkuId(Long skuId); + + /** + * 获得指定库区的入库单明细数量 + * + * @param areaId 库区编号 + * @return 入库单明细数量 + */ + long getReceiptOrderDetailCountByAreaId(Long areaId); + } diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailServiceImpl.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailServiceImpl.java index 25703e0..c773740 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailServiceImpl.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailServiceImpl.java @@ -111,6 +111,16 @@ public class WmsReceiptOrderDetailServiceImpl implements WmsReceiptOrderDetailSe return details; } + @Override + public long getReceiptOrderDetailCountBySkuId(Long skuId) { + return receiptOrderDetailMapper.selectCountBySkuId(skuId); + } + + @Override + public long getReceiptOrderDetailCountByAreaId(Long areaId) { + return receiptOrderDetailMapper.selectCountByAreaId(areaId); + } + private List buildReceiptOrderDetailList(WmsReceiptOrderSaveReqVO reqVO) { if (CollUtil.isEmpty(reqVO.getDetails())) { return ListUtil.of(); diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderService.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderService.java index f17a441..8b28e16 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderService.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderService.java @@ -65,4 +65,28 @@ public interface WmsReceiptOrderService { */ PageResult getReceiptOrderPage(WmsReceiptOrderPageReqVO pageReqVO); + /** + * 获得指定往来企业的入库单数量 + * + * @param merchantId 往来企业编号 + * @return 入库单数量 + */ + long getReceiptOrderCountByMerchantId(Long merchantId); + + /** + * 获得指定仓库的入库单数量 + * + * @param warehouseId 仓库编号 + * @return 入库单数量 + */ + long getReceiptOrderCountByWarehouseId(Long warehouseId); + + /** + * 获得指定库区的入库单数量 + * + * @param areaId 库区编号 + * @return 入库单数量 + */ + long getReceiptOrderCountByAreaId(Long areaId); + } diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderServiceImpl.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderServiceImpl.java index 2d1f334..77ff8e7 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderServiceImpl.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderServiceImpl.java @@ -128,6 +128,21 @@ public class WmsReceiptOrderServiceImpl implements WmsReceiptOrderService { return receiptOrderMapper.selectPage(pageReqVO); } + @Override + public long getReceiptOrderCountByMerchantId(Long merchantId) { + return receiptOrderMapper.selectCountByMerchantId(merchantId); + } + + @Override + public long getReceiptOrderCountByWarehouseId(Long warehouseId) { + return receiptOrderMapper.selectCountByWarehouseId(warehouseId); + } + + @Override + public long getReceiptOrderCountByAreaId(Long areaId) { + return receiptOrderMapper.selectCountByAreaId(areaId); + } + private void validateReceiptOrderSaveData(WmsReceiptOrderSaveReqVO reqVO) { // 校验入库单号唯一 validateReceiptOrderNoUnique(reqVO.getId(), reqVO.getNo()); diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailService.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailService.java index 0ef6623..8f594fa 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailService.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailService.java @@ -60,4 +60,20 @@ public interface WmsShipmentOrderDetailService { */ List validateShipmentOrderDetailListExists(Long orderId); + /** + * 获得指定 SKU 的出库单明细数量 + * + * @param skuId SKU 编号 + * @return 出库单明细数量 + */ + long getShipmentOrderDetailCountBySkuId(Long skuId); + + /** + * 获得指定库区的出库单明细数量 + * + * @param areaId 库区编号 + * @return 出库单明细数量 + */ + long getShipmentOrderDetailCountByAreaId(Long areaId); + } diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailServiceImpl.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailServiceImpl.java index 4c2b39a..484850f 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailServiceImpl.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailServiceImpl.java @@ -111,6 +111,16 @@ public class WmsShipmentOrderDetailServiceImpl implements WmsShipmentOrderDetail return details; } + @Override + public long getShipmentOrderDetailCountBySkuId(Long skuId) { + return shipmentOrderDetailMapper.selectCountBySkuId(skuId); + } + + @Override + public long getShipmentOrderDetailCountByAreaId(Long areaId) { + return shipmentOrderDetailMapper.selectCountByAreaId(areaId); + } + private List buildShipmentOrderDetailList(WmsShipmentOrderSaveReqVO reqVO) { if (CollUtil.isEmpty(reqVO.getDetails())) { return ListUtil.of(); diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderService.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderService.java index 2f8f5a2..ffe6450 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderService.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderService.java @@ -65,4 +65,28 @@ public interface WmsShipmentOrderService { */ PageResult getShipmentOrderPage(WmsShipmentOrderPageReqVO pageReqVO); + /** + * 获得指定往来企业的出库单数量 + * + * @param merchantId 往来企业编号 + * @return 出库单数量 + */ + long getShipmentOrderCountByMerchantId(Long merchantId); + + /** + * 获得指定仓库的出库单数量 + * + * @param warehouseId 仓库编号 + * @return 出库单数量 + */ + long getShipmentOrderCountByWarehouseId(Long warehouseId); + + /** + * 获得指定库区的出库单数量 + * + * @param areaId 库区编号 + * @return 出库单数量 + */ + long getShipmentOrderCountByAreaId(Long areaId); + } diff --git a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderServiceImpl.java b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderServiceImpl.java index 9cd7253..417f988 100644 --- a/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderServiceImpl.java +++ b/yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderServiceImpl.java @@ -129,6 +129,21 @@ public class WmsShipmentOrderServiceImpl implements WmsShipmentOrderService { return shipmentOrderMapper.selectPage(pageReqVO); } + @Override + public long getShipmentOrderCountByMerchantId(Long merchantId) { + return shipmentOrderMapper.selectCountByMerchantId(merchantId); + } + + @Override + public long getShipmentOrderCountByWarehouseId(Long warehouseId) { + return shipmentOrderMapper.selectCountByWarehouseId(warehouseId); + } + + @Override + public long getShipmentOrderCountByAreaId(Long areaId) { + return shipmentOrderMapper.selectCountByAreaId(areaId); + } + private void validateShipmentOrderSaveData(WmsShipmentOrderSaveReqVO reqVO) { // 校验出库单号唯一 validateShipmentOrderNoUnique(reqVO.getId(), reqVO.getNo()); diff --git a/yudao-module-wms/src/test/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImplTest.java b/yudao-module-wms/src/test/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImplTest.java index 5a94c09..ed0c3de 100644 --- a/yudao-module-wms/src/test/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImplTest.java +++ b/yudao-module-wms/src/test/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImplTest.java @@ -3,9 +3,11 @@ package cn.iocoder.yudao.module.wms.service.inventory; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.WmsInventoryPageReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.inventory.WmsInventoryDetailDO; import cn.iocoder.yudao.module.wms.dal.dataobject.inventory.WmsInventoryHistoryDO; import cn.iocoder.yudao.module.wms.dal.dataobject.inventory.WmsInventoryDO; import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.mysql.inventory.WmsInventoryDetailMapper; import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; import cn.iocoder.yudao.module.wms.dal.mysql.inventory.WmsInventoryHistoryMapper; import cn.iocoder.yudao.module.wms.dal.mysql.inventory.WmsInventoryMapper; @@ -19,8 +21,12 @@ import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.List; +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.INVENTORY_DETAIL_REMAIN_QUANTITY_NOT_ENOUGH; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.INVENTORY_QUANTITY_NOT_ENOUGH; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -34,6 +40,8 @@ public class WmsInventoryServiceImplTest extends BaseDbUnitTest { @Resource private WmsInventoryMapper inventoryMapper; @Resource + private WmsInventoryDetailMapper inventoryDetailMapper; + @Resource private WmsInventoryHistoryMapper inventoryHistoryMapper; @Resource private WmsItemMapper itemMapper; @@ -97,6 +105,72 @@ public class WmsInventoryServiceImplTest extends BaseDbUnitTest { } @Test + public void testChangeInventory_quantityNotEnough() { + // mock 数据 + wmsProperties.setBatchEnable(false); + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + inventoryMapper.insert(createInventory(sku.getId(), 100L, 1001L, "2.00")); + WmsInventoryChangeReqDTO reqDTO = createChangeReq(sku.getId(), 100L, 1001L, "-3.00"); + + // 调用,并断言 + assertServiceException(() -> inventoryService.changeInventory(reqDTO), INVENTORY_QUANTITY_NOT_ENOUGH, + new BigDecimal("2.000000"), new BigDecimal("-3.00")); + } + + @Test + public void testChangeInventory_decreaseInventoryDetail() { + // mock 数据 + wmsProperties.setBatchEnable(true); + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + inventoryMapper.insert(createInventory(sku.getId(), 100L, 1001L, "10.00")); + WmsInventoryDetailDO detail = createInventoryDetail(sku.getId(), 100L, 1001L, "5.00"); + inventoryDetailMapper.insert(detail); + WmsInventoryChangeReqDTO reqDTO = createChangeReq(sku.getId(), 100L, 1001L, "-3.00"); + reqDTO.getItems().get(0).setInventoryDetailId(detail.getId()); + + // 调用 + inventoryService.changeInventory(reqDTO); + + // 断言:库存余额 + WmsInventoryDO inventory = inventoryMapper.selectBySkuIdAndWarehouseIdAndAreaId(sku.getId(), 100L, 1001L); + assertEquals(0, new BigDecimal("7.00").compareTo(inventory.getQuantity())); + // 断言:库存明细 + WmsInventoryDetailDO dbDetail = inventoryDetailMapper.selectById(detail.getId()); + assertEquals(0, new BigDecimal("2.00").compareTo(dbDetail.getRemainQuantity())); + // 断言:库存流水 + List histories = inventoryHistoryMapper.selectList(); + assertEquals(1, histories.size()); + assertEquals(0, new BigDecimal("-3.00").compareTo(histories.get(0).getQuantity())); + } + + @Test + public void testChangeInventory_decreaseInventoryDetailRemainQuantityNotEnough() { + // mock 数据 + wmsProperties.setBatchEnable(true); + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + inventoryMapper.insert(createInventory(sku.getId(), 100L, 1001L, "10.00")); + WmsInventoryDetailDO detail = createInventoryDetail(sku.getId(), 100L, 1001L, "5.00"); + inventoryDetailMapper.insert(detail); + WmsInventoryChangeReqDTO reqDTO = createChangeReq(sku.getId(), 100L, 1001L, "-3.00"); + reqDTO.getItems().get(0).setInventoryDetailId(detail.getId()); + List items = new ArrayList<>(reqDTO.getItems()); + items.add(new WmsInventoryChangeReqDTO.Item() + .setSkuId(sku.getId()) + .setWarehouseId(100L) + .setAreaId(1001L) + .setInventoryDetailId(detail.getId()) + .setQuantity(new BigDecimal("-3.00"))); + reqDTO.setItems(items); + + // 调用,并断言:相同库存明细会合并后校验 + assertServiceException(() -> inventoryService.changeInventory(reqDTO), + INVENTORY_DETAIL_REMAIN_QUANTITY_NOT_ENOUGH, new BigDecimal("5.000000"), new BigDecimal("6.00")); + } + + @Test public void testGetInventoryPage_groupByWarehouse() { // mock 数据 WmsItemDO item = createItem("ITEM-001", "红富士苹果"); @@ -191,6 +265,17 @@ public class WmsInventoryServiceImplTest extends BaseDbUnitTest { .build(); } + private static WmsInventoryDetailDO createInventoryDetail(Long skuId, Long warehouseId, Long areaId, + String remainQuantity) { + return WmsInventoryDetailDO.builder() + .skuId(skuId) + .warehouseId(warehouseId) + .areaId(areaId) + .quantity(new BigDecimal(remainQuantity)) + .remainQuantity(new BigDecimal(remainQuantity)) + .build(); + } + private static WmsInventoryChangeReqDTO createChangeReq(Long skuId, Long warehouseId, Long areaId, String quantity) { return new WmsInventoryChangeReqDTO() .setOrderId(1L)