package cn.iocoder.yudao.module.mes.service.qc.ncr;
|
|
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrDisposeReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrDisposeRespVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.wm.miscissue.vo.MesWmMiscIssueSaveReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.wm.miscissue.vo.line.MesWmMiscIssueLineSaveReqVO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ncr.MesQcNcrDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcDO;
|
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.miscissue.MesWmMiscIssueDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesLineDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transaction.MesWmTransactionDO;
|
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.wm.miscissue.MesWmMiscIssueMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.wm.transaction.MesWmTransactionMapper;
|
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
|
import cn.iocoder.yudao.module.mes.enums.qc.MesNcrDispositionEnum;
|
import cn.iocoder.yudao.module.mes.enums.wm.MesWmMiscIssueStatusEnum;
|
import cn.iocoder.yudao.module.mes.enums.wm.MesWmMiscIssueTypeEnum;
|
import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductSalesStatusEnum;
|
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
|
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
|
import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderStatusEnum;
|
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
|
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
|
import cn.iocoder.yudao.module.mes.service.wm.miscissue.MesWmMiscIssueLineService;
|
import cn.iocoder.yudao.module.mes.service.wm.miscissue.MesWmMiscIssueService;
|
import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesService;
|
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
import org.junit.jupiter.api.Test;
|
import org.mockito.ArgumentCaptor;
|
import org.mockito.InOrder;
|
import org.springframework.test.util.ReflectionTestUtils;
|
|
import java.math.BigDecimal;
|
import java.util.List;
|
|
import static org.junit.jupiter.api.Assertions.*;
|
import static org.mockito.ArgumentMatchers.*;
|
import static org.mockito.Mockito.*;
|
|
class MesQcNcrOqcReworkServiceTest {
|
|
private static final long NCR_ID = 100L;
|
private static final long QC_ID = 1L;
|
private static final long SALES_ID = 4L;
|
private static final long SALES_LINE_ID = 5L;
|
private static final long BATCH_ID = 6L;
|
private static final long ORDER_ID = 7L;
|
private static final long STOCK_ID = 9L;
|
|
private MesQcNcrOqcReworkService newService() {
|
var service = new MesQcNcrOqcReworkService();
|
ReflectionTestUtils.setField(service, "batchMapper", mock(MesWmBatchMapper.class));
|
ReflectionTestUtils.setField(service, "orderMapper", mock(MesProWorkOrderMapper.class));
|
ReflectionTestUtils.setField(service, "oqcMapper", mock(MesQcOqcMapper.class));
|
ReflectionTestUtils.setField(service, "stockMapper", mock(MesWmMaterialStockMapper.class));
|
ReflectionTestUtils.setField(service, "miscIssueMapper", mock(MesWmMiscIssueMapper.class));
|
ReflectionTestUtils.setField(service, "transactionMapper", mock(MesWmTransactionMapper.class));
|
ReflectionTestUtils.setField(service, "salesMapper", mock(MesWmProductSalesMapper.class));
|
ReflectionTestUtils.setField(service, "stockService", mock(MesWmMaterialStockService.class));
|
ReflectionTestUtils.setField(service, "salesService", mock(MesWmProductSalesService.class));
|
ReflectionTestUtils.setField(service, "miscIssueService", mock(MesWmMiscIssueService.class));
|
ReflectionTestUtils.setField(service, "miscIssueLineService", mock(MesWmMiscIssueLineService.class));
|
ReflectionTestUtils.setField(service, "autoCodeRecordService", mock(MesMdAutoCodeRecordService.class));
|
ReflectionTestUtils.setField(service, "itemService", mock(cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService.class));
|
ReflectionTestUtils.setField(service, "reproductionService", mock(MesQcNcrOqcReproductionService.class));
|
ReflectionTestUtils.setField(service, "warehouseService", mock(cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService.class));
|
ReflectionTestUtils.setField(service, "warehouseLocationService", mock(cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService.class));
|
ReflectionTestUtils.setField(service, "warehouseAreaService", mock(cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService.class));
|
return service;
|
}
|
|
private static <T> T field(MesQcNcrOqcReworkService service, String name, Class<T> type) {
|
return type.cast(ReflectionTestUtils.getField(service, name));
|
}
|
|
private MesQcNcrDO ncr() {
|
return new MesQcNcrDO().setId(NCR_ID).setCode("NCR1").setQcId(QC_ID)
|
.setItemId(2L).setUnqualifiedQuantity(BigDecimal.valueOf(3));
|
}
|
|
private MesQcNcrOqcSourceClaim.OqcClaimedSource source() {
|
var qc = new MesQcOqcDO().setId(QC_ID).setSourceDocId(SALES_ID).setSourceLineId(SALES_LINE_ID);
|
var sales = new MesWmProductSalesDO().setId(SALES_ID).setCode("SALES1").setClientId(40L)
|
.setStatus(MesWmProductSalesStatusEnum.CONFIRMED.getStatus());
|
var line = new MesWmProductSalesLineDO().setId(SALES_LINE_ID).setSalesId(SALES_ID)
|
.setOqcId(QC_ID).setItemId(2L).setBatchId(BATCH_ID).setBatchCode("B1")
|
.setQualityStatus(MesWmQualityStatusEnum.FAIL.getStatus()).setQuantity(BigDecimal.TEN);
|
return new MesQcNcrOqcSourceClaim.OqcClaimedSource(qc, sales, line);
|
}
|
|
private static MesWmMaterialStockDO stock(BigDecimal quantity) {
|
return new MesWmMaterialStockDO().setId(STOCK_ID).setItemId(2L).setBatchId(BATCH_ID).setBatchCode("B1")
|
.setWarehouseId(11L).setLocationId(12L).setAreaId(13L).setQuantity(quantity)
|
.setFrozenQuantity(BigDecimal.ZERO).setReservedQuantity(BigDecimal.ZERO);
|
}
|
|
private void stubBatchAndOrder(MesQcNcrOqcReworkService service) {
|
when(field(service, "batchMapper", MesWmBatchMapper.class).selectById(BATCH_ID))
|
.thenReturn(new MesWmBatchDO().setId(BATCH_ID).setCode("B1").setItemId(2L)
|
.setClientId(40L).setWorkOrderId(ORDER_ID));
|
when(field(service, "orderMapper", MesProWorkOrderMapper.class).selectById(ORDER_ID))
|
.thenReturn(new MesProWorkOrderDO().setId(ORDER_ID).setProductId(8L).setClientId(40L));
|
}
|
|
@Test
|
void disposeDeductsSelectedStockCancelsSalesAndCreatesReworkOrder() {
|
var service = newService();
|
var source = source();
|
stubBatchAndOrder(service);
|
var salesService = field(service, "salesService", MesWmProductSalesService.class);
|
var stockService = field(service, "stockService", MesWmMaterialStockService.class);
|
var stockMapper = field(service, "stockMapper", MesWmMaterialStockMapper.class);
|
var miscIssueService = field(service, "miscIssueService", MesWmMiscIssueService.class);
|
var miscLineService = field(service, "miscIssueLineService", MesWmMiscIssueLineService.class);
|
var autoCode = field(service, "autoCodeRecordService", MesMdAutoCodeRecordService.class);
|
var reproduction = field(service, "reproductionService", MesQcNcrOqcReproductionService.class);
|
|
when(stockService.getMaterialStock(STOCK_ID)).thenReturn(stock(BigDecimal.TEN));
|
when(stockMapper.lockById(STOCK_ID)).thenReturn(stock(BigDecimal.TEN));
|
when(autoCode.generateAutoCode(anyString())).thenReturn("MI1");
|
when(miscIssueService.createMiscIssue(any(MesWmMiscIssueSaveReqVO.class))).thenReturn(20L);
|
var result = new MesQcNcrDisposeRespVO();
|
result.setDocCode("WO1");
|
when(reproduction.createReworkFromOriginal(any(), anyInt(), any(), any(), any(), any())).thenReturn(result);
|
|
var req = new MesQcNcrDisposeReqVO();
|
req.setId(NCR_ID);
|
req.setDisposition(MesNcrDispositionEnum.REWORK.getType());
|
req.setMaterialStockId(STOCK_ID);
|
|
var resp = service.dispose(ncr(), source, req);
|
|
var issueCaptor = ArgumentCaptor.forClass(MesWmMiscIssueSaveReqVO.class);
|
verify(miscIssueService).createMiscIssue(issueCaptor.capture());
|
assertEquals(MesWmMiscIssueTypeEnum.REWORK.getType(), issueCaptor.getValue().getType());
|
assertEquals("QC_NCR", issueCaptor.getValue().getSourceDocType());
|
assertEquals(NCR_ID, issueCaptor.getValue().getSourceDocId().longValue());
|
|
var lineCaptor = ArgumentCaptor.forClass(MesWmMiscIssueLineSaveReqVO.class);
|
verify(miscLineService).createMiscIssueLine(lineCaptor.capture());
|
assertEquals(STOCK_ID, lineCaptor.getValue().getMaterialStockId().longValue());
|
assertEquals(BATCH_ID, lineCaptor.getValue().getBatchId().longValue());
|
assertEquals(0, BigDecimal.valueOf(3).compareTo(lineCaptor.getValue().getQuantity()));
|
|
InOrder order = inOrder(stockMapper, salesService, miscIssueService, reproduction);
|
order.verify(stockMapper).lockById(STOCK_ID);
|
order.verify(salesService).cancelProductSales(SALES_ID);
|
order.verify(miscIssueService).finishMiscIssue(20L);
|
order.verify(reproduction).createReworkFromOriginal(any(), anyInt(), any(), any(), any(), any());
|
|
assertNotNull(resp.getMessage());
|
assertTrue(resp.getMessage().contains("MI1"));
|
assertTrue(resp.getMessage().contains("SALES1"));
|
assertTrue(resp.getMessage().contains("WO1"));
|
}
|
|
@Test
|
void disposeRejectsWhenStockAvailableIsInsufficient() {
|
var service = newService();
|
stubBatchAndOrder(service);
|
var stockService = field(service, "stockService", MesWmMaterialStockService.class);
|
var stockMapper = field(service, "stockMapper", MesWmMaterialStockMapper.class);
|
when(stockService.getMaterialStock(STOCK_ID)).thenReturn(stock(BigDecimal.ONE));
|
when(stockMapper.lockById(STOCK_ID)).thenReturn(stock(BigDecimal.ONE));
|
|
var req = new MesQcNcrDisposeReqVO();
|
req.setDisposition(MesNcrDispositionEnum.REWORK.getType());
|
req.setMaterialStockId(STOCK_ID);
|
|
var ex = assertThrows(ServiceException.class, () -> service.dispose(ncr(), source(), req));
|
assertTrue(ex.getMessage().contains("可用量"));
|
verify(field(service, "salesService", MesWmProductSalesService.class), never()).cancelProductSales(any());
|
}
|
|
@Test
|
void disposeRejectsAmbiguousStockWhenNoStockSelected() {
|
var service = newService();
|
stubBatchAndOrder(service);
|
var stockMapper = field(service, "stockMapper", MesWmMaterialStockMapper.class);
|
when(stockMapper.selectList(any(Wrapper.class))).thenReturn(List.of(
|
stock(BigDecimal.TEN).setId(9L).setWarehouseId(11L),
|
stock(BigDecimal.TEN).setId(10L).setWarehouseId(21L)));
|
|
var req = new MesQcNcrDisposeReqVO();
|
req.setDisposition(MesNcrDispositionEnum.REPAIR.getType());
|
|
var ex = assertThrows(ServiceException.class, () -> service.dispose(ncr(), source(), req));
|
assertTrue(ex.getMessage().contains("返工库存"));
|
verify(field(service, "salesService", MesWmProductSalesService.class), never()).cancelProductSales(any());
|
}
|
|
@Test
|
void validateBeforeCloseRequiresFinishedOrderCancelledSalesAndMatchingIssue() {
|
var service = newService();
|
var ncr = ncr();
|
var orders = field(service, "orderMapper", MesProWorkOrderMapper.class);
|
var oqcs = field(service, "oqcMapper", MesQcOqcMapper.class);
|
var salesMapper = field(service, "salesMapper", MesWmProductSalesMapper.class);
|
var issues = field(service, "miscIssueMapper", MesWmMiscIssueMapper.class);
|
var flows = field(service, "transactionMapper", MesWmTransactionMapper.class);
|
|
// 1. 找不到返工工单
|
when(orders.selectList(any(Wrapper.class))).thenReturn(List.of());
|
assertThrows(ServiceException.class, () -> service.validateBeforeClose(ncr));
|
|
// 2. 工单未完成
|
var order = new MesProWorkOrderDO().setId(ORDER_ID).setCode("WO1");
|
when(orders.selectList(any(Wrapper.class))).thenReturn(List.of(order));
|
assertThrows(ServiceException.class, () -> service.validateBeforeClose(ncr));
|
order.setStatus(MesProWorkOrderStatusEnum.FINISHED.getStatus())
|
.setFinishDate(java.time.LocalDateTime.now());
|
|
// 3. 原销售出库单未取消
|
when(oqcs.selectById(QC_ID)).thenReturn(new MesQcOqcDO().setId(QC_ID).setSourceDocId(SALES_ID));
|
var sales = new MesWmProductSalesDO().setId(SALES_ID).setCode("SALES1")
|
.setStatus(MesWmProductSalesStatusEnum.CONFIRMED.getStatus());
|
when(salesMapper.selectById(SALES_ID)).thenReturn(sales);
|
assertThrows(ServiceException.class, () -> service.validateBeforeClose(ncr));
|
sales.setStatus(MesWmProductSalesStatusEnum.CANCELED.getStatus());
|
|
// 4. 返工领用出库单缺失
|
when(issues.selectList(any(Wrapper.class))).thenReturn(List.of());
|
assertThrows(ServiceException.class, () -> service.validateBeforeClose(ncr));
|
var issue = new MesWmMiscIssueDO().setId(20L).setCode("MI1")
|
.setStatus(MesWmMiscIssueStatusEnum.FINISHED.getStatus());
|
when(issues.selectList(any(Wrapper.class))).thenReturn(List.of(issue));
|
|
// 5. 出库流水数量不符
|
when(flows.selectList(any(Wrapper.class))).thenReturn(List.of(new MesWmTransactionDO()
|
.setItemId(2L).setType(MesWmTransactionTypeEnum.OUT.getType())
|
.setQuantity(BigDecimal.valueOf(-2))));
|
assertThrows(ServiceException.class, () -> service.validateBeforeClose(ncr));
|
|
// 6. 全部满足
|
when(flows.selectList(any(Wrapper.class))).thenReturn(List.of(new MesWmTransactionDO()
|
.setItemId(2L).setType(MesWmTransactionTypeEnum.OUT.getType())
|
.setQuantity(BigDecimal.valueOf(-3))));
|
assertDoesNotThrow(() -> service.validateBeforeClose(ncr));
|
}
|
}
|