diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java index 188c1cf..08945b0 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java @@ -356,6 +356,13 @@ public interface ErrorCodeConstants { ErrorCode WM_MATERIAL_STOCK_NOT_EXISTS = new ErrorCode(1_040_703_000, "库存记录不存在"); ErrorCode WM_MATERIAL_STOCK_INSUFFICIENT = new ErrorCode(1_040_703_001, "库存数量不足"); ErrorCode WM_TRANSACTION_TYPE_NOT_EXISTS = new ErrorCode(1_040_703_002, "库存事务类型不存在"); + ErrorCode WM_TRANSACTION_WAREHOUSE_FROZEN = new ErrorCode(1_040_703_003, "仓库({})已被冻结"); + ErrorCode WM_TRANSACTION_LOCATION_FROZEN = new ErrorCode(1_040_703_004, "库区({})已被冻结"); + ErrorCode WM_TRANSACTION_AREA_FROZEN = new ErrorCode(1_040_703_005, "库位({})已被冻结"); + ErrorCode WM_TRANSACTION_STOCK_FROZEN = new ErrorCode(1_040_703_006, "存放于({}/{}/{})下的物料已被冻结"); + ErrorCode WM_TRANSACTION_BATCH_REQUIRED = new ErrorCode(1_040_703_007, "当前物料启用了批次管理,批次号不能为空"); + ErrorCode WM_MATERIAL_STOCK_AREA_ITEM_MIXING_NOT_ALLOWED = new ErrorCode(1_040_703_008, "库位({})不允许物料混放,请选择其他库位"); + ErrorCode WM_MATERIAL_STOCK_AREA_BATCH_MIXING_NOT_ALLOWED = new ErrorCode(1_040_703_009, "库位({})不允许批次混放,请选择其他库位"); // ========== MES 仓库管理-到货通知单(1-040-704-000) ========== ErrorCode WM_ARRIVAL_NOTICE_NOT_EXISTS = new ErrorCode(1_040_704_000, "到货通知单不存在"); diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockService.java index f79da0a..794298c 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockService.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockService.java @@ -29,6 +29,27 @@ public interface MesWmMaterialStockService { MesWmMaterialStockDO getMaterialStock(Long id); /** + * 按组合键查询库存记录 + * + * @param itemId 物料编号 + * @param warehouseId 仓库编号 + * @param locationId 库区编号 + * @param areaId 库位编号 + * @param batchId 批次编号 + * @return 库存记录,不存在返回 null + */ + MesWmMaterialStockDO getMaterialStockByCompositeKey(Long itemId, Long warehouseId, Long locationId, + Long areaId, Long batchId); + + /** + * 查询指定库位的库存记录列表 + * + * @param areaId 库位编号 + * @return 库存记录列表 + */ + List getMaterialStockListByAreaId(Long areaId); + + /** * 获得库存台账分页 * * @param pageReqVO 分页参数 @@ -97,10 +118,10 @@ public interface MesWmMaterialStockService { * @param batchId 批次编号 * @param vendorId 供应商编号 * @param receiptTime 入库时间(为空则默认当前时间) - * @return 库存记录编号 + * @return 库存记录 */ - Long getOrCreateMaterialStock(Long itemId, Long warehouseId, Long locationId, Long areaId, - Long batchId, Long vendorId, LocalDateTime receiptTime); + MesWmMaterialStockDO getOrCreateMaterialStock(Long itemId, Long warehouseId, Long locationId, Long areaId, + Long batchId, Long vendorId, LocalDateTime receiptTime); /** * 更新库存数量 @@ -111,4 +132,15 @@ public interface MesWmMaterialStockService { */ void updateMaterialStockQuantity(Long materialStockId, BigDecimal quantity, boolean checkFlag); + /** + * 库位混放规则校验 + * + * 根据库位配置的物料和批次混放规则,检查是否允许在该库位存放指定物料/批次 + * + * @param areaId 库位编号 + * @param itemId 物料编号 + * @param batchId 批次编号(可为 null) + */ + void checkAreaMixingRule(Long areaId, Long itemId, Long batchId); + } diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java index 48a97dd..3a5589f 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.mes.service.wm.materialstock; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.SetUtils; import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockFreezeReqVO; @@ -9,9 +10,11 @@ import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMat import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO; import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper; import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService; +import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -22,8 +25,7 @@ import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; -import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_MATERIAL_STOCK_INSUFFICIENT; -import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_MATERIAL_STOCK_NOT_EXISTS; +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; /** * MES 库存台账 Service 实现类 @@ -37,15 +39,27 @@ public class MesWmMaterialStockServiceImpl implements MesWmMaterialStockService @Resource private MesMdItemService itemService; - @Resource private MesMdItemTypeService itemTypeService; + @Resource + private MesWmWarehouseAreaService areaService; @Override public MesWmMaterialStockDO getMaterialStock(Long id) { return materialStockMapper.selectById(id); } + @Override + public MesWmMaterialStockDO getMaterialStockByCompositeKey(Long itemId, Long warehouseId, Long locationId, + Long areaId, Long batchId) { + return materialStockMapper.selectByCompositeKey(itemId, warehouseId, locationId, areaId, batchId); + } + + @Override + public List getMaterialStockListByAreaId(Long areaId) { + return materialStockMapper.selectList(MesWmMaterialStockDO::getAreaId, areaId); + } + public MesWmMaterialStockDO validateMaterialStockExists(Long id) { MesWmMaterialStockDO stock = materialStockMapper.selectById(id); if (stock == null) { @@ -113,13 +127,13 @@ public class MesWmMaterialStockServiceImpl implements MesWmMaterialStockService } @Override - public Long getOrCreateMaterialStock(Long itemId, Long warehouseId, Long locationId, Long areaId, - Long batchId, Long vendorId, LocalDateTime receiptTime) { + public MesWmMaterialStockDO getOrCreateMaterialStock(Long itemId, Long warehouseId, Long locationId, Long areaId, + Long batchId, Long vendorId, LocalDateTime receiptTime) { // 1. 查找已有库存记录 MesWmMaterialStockDO stock = materialStockMapper.selectByCompositeKey( itemId, warehouseId, locationId, areaId, batchId); if (stock != null) { - return stock.getId(); + return stock; } // 2. 不存在则新建 @@ -133,7 +147,7 @@ public class MesWmMaterialStockServiceImpl implements MesWmMaterialStockService .frozenFlag(false) .build(); materialStockMapper.insert(newStock); - return newStock.getId(); + return newStock; } @Override @@ -145,4 +159,28 @@ public class MesWmMaterialStockServiceImpl implements MesWmMaterialStockService } } + @Override + public void checkAreaMixingRule(Long areaId, Long itemId, Long batchId) { + // 1.1 校验库位必须存在 + MesWmWarehouseAreaDO area = areaService.validateWarehouseAreaExists(areaId); + + // 2. 查询当前库位上已有的库存记录 + List stockList = getMaterialStockListByAreaId(areaId); + if (CollUtil.isEmpty(stockList)) { + return; + } + // 2.1 检查物料混放 + if (Boolean.FALSE.equals(area.getAllowItemMixing()) && itemId != null) { + if (CollUtil.anyMatch(stockList, item -> ObjUtil.notEqual(itemId, item.getItemId()))) { + throw exception(WM_MATERIAL_STOCK_AREA_ITEM_MIXING_NOT_ALLOWED, area.getName()); + } + } + // 2.2 检查批次混放 + if (Boolean.FALSE.equals(area.getAllowBatchMixing()) && batchId != null) { + if (CollUtil.anyMatch(stockList, item -> ObjUtil.notEqual(batchId, item.getBatchId()))) { + throw exception(WM_MATERIAL_STOCK_AREA_BATCH_MIXING_NOT_ALLOWED, area.getName()); + } + } + } + } diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java index 9bb142f..5cab228 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java @@ -3,13 +3,22 @@ package cn.iocoder.yudao.module.mes.service.wm.transaction; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transaction.MesWmTransactionDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.transaction.MesWmTransactionMapper; import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum; +import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService; import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService; import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO; +import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService; +import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService; +import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,6 +28,9 @@ import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; + /** * MES 库存事务流水 Service 实现类 */ @@ -28,11 +40,18 @@ public class MesWmTransactionServiceImpl implements MesWmTransactionService { @Resource private MesWmTransactionMapper transactionMapper; - @Resource private MesWmMaterialStockService materialStockService; @Resource private MesWmBatchService batchService; + @Resource + private MesMdItemService itemService; + @Resource + private MesWmWarehouseService warehouseService; + @Resource + private MesWmWarehouseLocationService locationService; + @Resource + private MesWmWarehouseAreaService areaService; @Override @Transactional(rollbackFor = Exception.class) @@ -42,23 +61,23 @@ public class MesWmTransactionServiceImpl implements MesWmTransactionService { Assert.notNull(typeEnum, "事务类型({}) 不存在", reqDTO.getType()); // 1.2 校验 quantity 正负号与事务方向一致(入库必须正数,出库必须负数) boolean inbound = typeEnum.isInbound(); + checkQuantity(reqDTO, inbound); + // 1.3 入库事务:批次号校验 + batchId/batchCode 互补 + checkBatch(reqDTO, inbound); + // 1.4 入库事务:库位混放规则校验 if (inbound) { - Assert.isTrue(reqDTO.getQuantity().compareTo(BigDecimal.ZERO) > 0, - "入库事务数量必须为正数,实际值: {}", reqDTO.getQuantity()); - } else { - Assert.isTrue(reqDTO.getQuantity().compareTo(BigDecimal.ZERO) < 0, - "出库事务数量必须为负数,实际值: {}", reqDTO.getQuantity()); + materialStockService.checkAreaMixingRule(reqDTO.getAreaId(), reqDTO.getItemId(), reqDTO.getBatchId()); } - // 1.3 batchId / batchCode 互补 - completeBatchInfo(reqDTO); // 2.1 获取或创建库存记录 - Long materialStockId = materialStockService.getOrCreateMaterialStock( + MesWmMaterialStockDO materialStock = materialStockService.getOrCreateMaterialStock( reqDTO.getItemId(), reqDTO.getWarehouseId(), reqDTO.getLocationId(), reqDTO.getAreaId(), reqDTO.getBatchId(), reqDTO.getVendorId(), reqDTO.getReceiptTime()); - // 2.2 更新库存数量 + // 2.2 冻结校验(仓库、库区、库位、库存记录) + checkFrozen(reqDTO, materialStock); + // 2.3 更新库存数量 boolean checkFlag = ObjUtil.defaultIfNull(reqDTO.getCheckFlag(), true); - materialStockService.updateMaterialStockQuantity(materialStockId, reqDTO.getQuantity(), checkFlag); + materialStockService.updateMaterialStockQuantity(materialStock.getId(), reqDTO.getQuantity(), checkFlag); // 3. 插入事务流水 MesWmTransactionDO transaction = MesWmTransactionDO.builder() @@ -66,7 +85,7 @@ public class MesWmTransactionServiceImpl implements MesWmTransactionService { .itemId(reqDTO.getItemId()).batchId(reqDTO.getBatchId()).batchCode(reqDTO.getBatchCode()) .warehouseId(reqDTO.getWarehouseId()).locationId(reqDTO.getLocationId()).areaId(reqDTO.getAreaId()) .bizType(reqDTO.getBizType()).bizId(reqDTO.getBizId()).bizCode(reqDTO.getBizCode()).bizLineId(reqDTO.getBizLineId()) - .materialStockId(materialStockId).relatedTransactionId(reqDTO.getRelatedTransactionId()) + .materialStockId(materialStock.getId()).relatedTransactionId(reqDTO.getRelatedTransactionId()) .build(); transactionMapper.insert(transaction); return transaction.getId(); @@ -78,30 +97,79 @@ public class MesWmTransactionServiceImpl implements MesWmTransactionService { reqDTOs.forEach(this::createTransaction); } + // ==================== 私有校验方法 ==================== + /** - * batchId / batchCode 互补:有 batchId 无 batchCode 时,查出 batchCode;反之亦然。 - * - * @param reqDTO 事务请求 DTO + * 校验数量正负号与事务方向一致 */ - private void completeBatchInfo(MesWmTransactionSaveReqDTO reqDTO) { - if (reqDTO.getBatchId() != null && StrUtil.isNotEmpty(reqDTO.getBatchCode())) { - return; + private void checkQuantity(MesWmTransactionSaveReqDTO reqDTO, boolean inbound) { + if (inbound) { + Assert.isTrue(reqDTO.getQuantity().compareTo(BigDecimal.ZERO) > 0, + "入库事务数量必须为正数,实际值: {}", reqDTO.getQuantity()); + } else { + Assert.isTrue(reqDTO.getQuantity().compareTo(BigDecimal.ZERO) < 0, + "出库事务数量必须为负数,实际值: {}", reqDTO.getQuantity()); } - // 情况一:有 batchId,补 batchCode - if (reqDTO.getBatchId() != null) { + } + + /** + * 批次校验:batchId/batchCode 互补 + 入库时批次号必填检查 + * + * @param reqDTO 事务请求 DTO + * @param inbound 是否入库 + */ + private void checkBatch(MesWmTransactionSaveReqDTO reqDTO, boolean inbound) { + // 1. batchId / batchCode 互补 + if (reqDTO.getBatchId() != null && StrUtil.isEmpty(reqDTO.getBatchCode())) { MesWmBatchDO batch = batchService.getBatch(reqDTO.getBatchId()); if (batch != null) { reqDTO.setBatchCode(batch.getCode()); } - return; - } - // 情况二:有 batchCode,补 batchId - if (StrUtil.isNotEmpty(reqDTO.getBatchCode())) { + } else if (StrUtil.isNotEmpty(reqDTO.getBatchCode()) && reqDTO.getBatchId() == null) { MesWmBatchDO batch = batchService.getBatchByCode(reqDTO.getBatchCode()); if (batch != null) { reqDTO.setBatchId(batch.getId()); } } + // 2. 入库时:启用批次管理的物料,必须传递批次号 + if (inbound) { + MesMdItemDO item = itemService.getItem(reqDTO.getItemId()); + if (item != null && Boolean.TRUE.equals(item.getBatchFlag())) { + if (StrUtil.isEmpty(reqDTO.getBatchCode()) && reqDTO.getBatchId() == null) { + throw exception(WM_TRANSACTION_BATCH_REQUIRED); + } + } + } + } + + /** + * 冻结校验:检查仓库、库区、库位、库存记录是否被冻结 + * + * @param reqDTO 事务请求 DTO + * @param materialStock 库存记录 + */ + private void checkFrozen(MesWmTransactionSaveReqDTO reqDTO, MesWmMaterialStockDO materialStock) { + // 1.1 检查仓库冻结 + MesWmWarehouseDO warehouse = warehouseService.validateWarehouseExists(reqDTO.getWarehouseId()); + if (Boolean.TRUE.equals(warehouse.getFrozen())) { + throw exception(WM_TRANSACTION_WAREHOUSE_FROZEN, warehouse.getName()); + } + // 1.2 检查库区冻结 + MesWmWarehouseLocationDO location = locationService.validateWarehouseLocationExists(reqDTO.getLocationId()); + if (Boolean.TRUE.equals(location.getFrozen())) { + throw exception(WM_TRANSACTION_LOCATION_FROZEN, location.getName()); + } + // 1.3 检查库位冻结 + MesWmWarehouseAreaDO area = areaService.validateWarehouseAreaExists(reqDTO.getAreaId()); + if (Boolean.TRUE.equals(area.getFrozen())) { + throw exception(WM_TRANSACTION_AREA_FROZEN, area.getName()); + } + + // 2. 检查库存记录冻结 + if (Boolean.TRUE.equals(materialStock.getFrozenFlag())) { + throw exception(WM_TRANSACTION_STOCK_FROZEN, + warehouse.getName(), location.getName(), area.getName()); + } } } diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/dto/MesWmTransactionSaveReqDTO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/dto/MesWmTransactionSaveReqDTO.java index ef2e68f..6b07093 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/dto/MesWmTransactionSaveReqDTO.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/dto/MesWmTransactionSaveReqDTO.java @@ -85,6 +85,7 @@ public class MesWmTransactionSaveReqDTO { /** * 库位 ID */ + @NotNull(message = "库位 ID 不能为空") private Long areaId; // ===== 台账补充字段(入库时用于初始化 MesWmMaterialStockDO) ===== diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/warehouse/MesWmWarehouseAreaServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/warehouse/MesWmWarehouseAreaServiceImpl.java index 8e58b57..acaeb34 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/warehouse/MesWmWarehouseAreaServiceImpl.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/warehouse/MesWmWarehouseAreaServiceImpl.java @@ -39,6 +39,7 @@ public class MesWmWarehouseAreaServiceImpl implements MesWmWarehouseAreaService @Lazy private MesMdWorkstationService workstationService; @Resource + @Lazy private MesWmMaterialStockService materialStockService; @Resource @Lazy