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 f2f805db39..25bbb0436a 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 @@ -467,6 +467,7 @@ public interface ErrorCodeConstants { ErrorCode WM_ITEM_RECEIPT_LINE_ARRIVAL_NOTICE_LINE_NOT_ALLOWED = new ErrorCode(1_040_705_102, "入库单未关联到货通知单,不能选择到货通知单行"); ErrorCode WM_ITEM_RECEIPT_DETAIL_NOT_EXISTS = new ErrorCode(1_040_705_200, "采购入库明细不存在"); + ErrorCode WM_ITEM_RECEIPT_DETAIL_LINE_MISMATCH = new ErrorCode(1_040_705_201, "入库单行与上架明细的单据或物料不一致"); ErrorCode WM_ITEM_RECEIPT_DETAIL_QUANTITY_EXCEED = new ErrorCode(1_040_705_202, "上架明细总数量不能超过行入库数量"); // ========== MES 仓库管理-领料申请单(1-040-706-000) ========== diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptDetailServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptDetailServiceImpl.java index 2d4f600abf..e0115f9bff 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptDetailServiceImpl.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptDetailServiceImpl.java @@ -8,6 +8,8 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceip import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemreceipt.MesWmItemReceiptDetailMapper; import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService; import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService; +import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; +import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService; import jakarta.annotation.Resource; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -15,6 +17,7 @@ import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.util.List; +import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; @@ -26,6 +29,11 @@ import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; @Validated public class MesWmItemReceiptDetailServiceImpl implements MesWmItemReceiptDetailService { + @Resource + private MesMdItemService itemService; + @Resource + private MesWmBatchService batchService; + @Resource private MesWmItemReceiptDetailMapper itemReceiptDetailMapper; @@ -62,6 +70,8 @@ public class MesWmItemReceiptDetailServiceImpl implements MesWmItemReceiptDetail MesWmItemReceiptDetailDO detail = validateItemReceiptDetailExists(updateReqVO.getId()); // 1.2 校验数据 updateReqVO.setReceiptId(detail.getReceiptId()); + updateReqVO.setLineId(detail.getLineId()); + updateReqVO.setItemId(detail.getItemId()); validateItemReceiptDetailSaveData(updateReqVO); // 2. 更新 @@ -75,6 +85,23 @@ public class MesWmItemReceiptDetailServiceImpl implements MesWmItemReceiptDetail private void validateItemReceiptDetailSaveData(MesWmItemReceiptDetailSaveReqVO reqVO) { // 校验父单据存在且为可编辑状态 itemReceiptService.validateItemReceiptEditable(reqVO.getReceiptId()); + // 上架明细沿用入库单行的物料和批次,避免客户端漏传或传错批次。 + MesWmItemReceiptLineDO line = itemReceiptLineService.getItemReceiptLine(reqVO.getLineId()); + if (line == null) { + throw exception(WM_ITEM_RECEIPT_LINE_NOT_EXISTS); + } + if (!Objects.equals(line.getReceiptId(), reqVO.getReceiptId()) + || !Objects.equals(line.getItemId(), reqVO.getItemId())) { + throw exception(WM_ITEM_RECEIPT_DETAIL_LINE_MISMATCH); + } + reqVO.setBatchId(line.getBatchId()); + if (Boolean.TRUE.equals(itemService.validateItemExists(reqVO.getItemId()).getBatchFlag()) + && reqVO.getBatchId() == null) { + throw exception(MD_ITEM_BATCH_REQUIRED); + } + if (reqVO.getBatchId() != null) { + batchService.validateBatchExists(reqVO.getBatchId(), reqVO.getItemId()); + } // 校验库区关系 warehouseAreaService.validateWarehouseAreaExists(reqVO.getWarehouseId(), reqVO.getLocationId(), reqVO.getAreaId()); // 校验库位物料/批次混放规则 diff --git a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptDetailServiceImplTest.java b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptDetailServiceImplTest.java new file mode 100644 index 0000000000..563f6c74f1 --- /dev/null +++ b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptDetailServiceImplTest.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.mes.service.wm.itemreceipt; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.mes.controller.admin.wm.itemreceipt.vo.detail.MesWmItemReceiptDetailSaveReqVO; +import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptLineDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptDetailDO; +import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemreceipt.MesWmItemReceiptDetailMapper; +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.warehouse.MesWmWarehouseAreaService; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.math.BigDecimal; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +class MesWmItemReceiptDetailServiceImplTest extends BaseMockitoUnitTest { + @InjectMocks private MesWmItemReceiptDetailServiceImpl service; + @Mock private MesWmItemReceiptDetailMapper itemReceiptDetailMapper; + @Mock private MesWmItemReceiptService itemReceiptService; + @Mock private MesWmItemReceiptLineService itemReceiptLineService; + @Mock private MesWmWarehouseAreaService warehouseAreaService; + @Mock private MesWmMaterialStockService materialStockService; + @Mock private MesMdItemService itemService; + @Mock private MesWmBatchService batchService; + + private MesWmItemReceiptDetailSaveReqVO prepare(Long batchId, boolean batchFlag) { + when(itemReceiptLineService.getItemReceiptLine(11L)).thenReturn( + new MesWmItemReceiptLineDO().setId(11L).setReceiptId(1L).setItemId(2L).setBatchId(batchId)); + when(itemService.validateItemExists(2L)).thenReturn(new MesMdItemDO().setId(2L).setBatchFlag(batchFlag)); + return new MesWmItemReceiptDetailSaveReqVO().setReceiptId(1L).setLineId(11L) + .setItemId(2L).setAreaId(3L).setQuantity(BigDecimal.ONE); + } + + @Test + void createInheritsLineBatchBeforeMixingValidation() { + MesWmItemReceiptDetailSaveReqVO request = prepare(22L, true).setBatchId(999L); + service.createItemReceiptDetail(request); + verify(batchService).validateBatchExists(22L, 2L); + verify(materialStockService).checkAreaMixingRule(3L, 2L, 22L); + verify(itemReceiptDetailMapper).insert(argThat((MesWmItemReceiptDetailDO row) -> Objects.equals(row.getBatchId(), 22L))); + } + + @Test + void missingLineBatchIsRejectedBeforeInsert() { + MesWmItemReceiptDetailSaveReqVO request = prepare(null, true); + assertServiceException(() -> service.createItemReceiptDetail(request), MD_ITEM_BATCH_REQUIRED); + verify(itemReceiptDetailMapper, never()).insert(any(MesWmItemReceiptDetailDO.class)); + } + + @Test + void unrelatedReceiptLineIsRejected() { + MesWmItemReceiptDetailSaveReqVO request = prepare(22L, true).setReceiptId(99L); + reset(itemService); + assertServiceException(() -> service.createItemReceiptDetail(request), WM_ITEM_RECEIPT_DETAIL_LINE_MISMATCH); + verifyNoInteractions(materialStockService); + } + + @Test + void nonBatchItemCanRemainWithoutBatch() { + service.createItemReceiptDetail(prepare(null, false)); + verifyNoInteractions(batchService); + verify(itemReceiptDetailMapper).insert(argThat((MesWmItemReceiptDetailDO row) -> row.getBatchId() == null)); + } +}