🎉 Initial commit

This commit is contained in:
2026-07-27 14:26:14 +08:00
commit fd54f07889
6935 changed files with 617410 additions and 0 deletions

View File

@@ -0,0 +1,280 @@
package cn.iocoder.yudao.module.mes.dal.mysql.pro.task;
import cn.hutool.core.collection.ListUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.route.MesProRouteProcessMapper;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.List;
import java.util.function.Consumer;
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* {@link MesProTaskMapper#selectPage(MesProTaskPageReqVO)} 的单元测试
*
* @author 芋道源码
*/
public class MesProTaskMapperTest extends BaseDbUnitTest {
@Resource
private MesProTaskMapper taskMapper;
@Resource
private MesProRouteProcessMapper routeProcessMapper;
/**
* 创建 Task 时统一设置 BigDecimal 字段为 2 位小数,避免 H2 decimal(14,2) 精度不匹配
*/
private MesProTaskDO createTaskPojo(Consumer<MesProTaskDO> consumer) {
return randomPojo(MesProTaskDO.class, o -> {
o.setQuantity(new BigDecimal("10.00"));
o.setProducedQuantity(new BigDecimal("5.00"));
o.setQualifyQuantity(new BigDecimal("4.00"));
o.setUnqualifyQuantity(new BigDecimal("1.00"));
o.setChangedQuantity(new BigDecimal("0.00"));
consumer.accept(o);
});
}
// ==================== selectPage 基础过滤 ====================
@Test
public void testSelectPage_byCode() {
MesProTaskDO matchTask = createTaskPojo(o -> {
o.setCode("PT202501010001");
o.setStatus(0);
});
taskMapper.insert(matchTask);
taskMapper.insert(cloneIgnoreId(matchTask, o -> o.setCode("OTHER_CODE")));
MesProTaskPageReqVO reqVO = new MesProTaskPageReqVO();
reqVO.setCode("PT2025");
PageResult<MesProTaskDO> result = taskMapper.selectPage(reqVO);
assertEquals(1, result.getTotal());
assertPojoEquals(matchTask, result.getList().get(0));
}
@Test
public void testSelectPage_byName() {
MesProTaskDO matchTask = createTaskPojo(o -> {
o.setName("注塑任务-A");
o.setStatus(0);
});
taskMapper.insert(matchTask);
taskMapper.insert(cloneIgnoreId(matchTask, o -> o.setName("OTHER_NAME")));
MesProTaskPageReqVO reqVO = new MesProTaskPageReqVO();
reqVO.setName("注塑");
PageResult<MesProTaskDO> result = taskMapper.selectPage(reqVO);
assertEquals(1, result.getTotal());
assertPojoEquals(matchTask, result.getList().get(0));
}
@Test
public void testSelectPage_byWorkOrderId() {
MesProTaskDO matchTask = createTaskPojo(o -> {
o.setWorkOrderId(100L);
o.setStatus(0);
});
taskMapper.insert(matchTask);
taskMapper.insert(cloneIgnoreId(matchTask, o -> o.setWorkOrderId(999L)));
MesProTaskPageReqVO reqVO = new MesProTaskPageReqVO();
reqVO.setWorkOrderId(100L);
PageResult<MesProTaskDO> result = taskMapper.selectPage(reqVO);
assertEquals(1, result.getTotal());
assertPojoEquals(matchTask, result.getList().get(0));
}
@Test
public void testSelectPage_byStatus() {
MesProTaskDO matchTask = createTaskPojo(o -> o.setStatus(1));
taskMapper.insert(matchTask);
taskMapper.insert(cloneIgnoreId(matchTask, o -> o.setStatus(2)));
MesProTaskPageReqVO reqVO = new MesProTaskPageReqVO();
reqVO.setStatus(1);
PageResult<MesProTaskDO> result = taskMapper.selectPage(reqVO);
assertEquals(1, result.getTotal());
assertPojoEquals(matchTask, result.getList().get(0));
}
@Test
public void testSelectPage_byStatuses() {
MesProTaskDO task1 = createTaskPojo(o -> o.setStatus(1));
MesProTaskDO task2 = createTaskPojo(o -> o.setStatus(2));
MesProTaskDO task3 = createTaskPojo(o -> o.setStatus(3));
taskMapper.insert(task1);
taskMapper.insert(task2);
taskMapper.insert(task3);
MesProTaskPageReqVO reqVO = new MesProTaskPageReqVO();
reqVO.setStatuses(ListUtil.of(1, 2));
PageResult<MesProTaskDO> result = taskMapper.selectPage(reqVO);
assertEquals(2, result.getTotal());
}
@Test
public void testSelectPage_noFilter() {
MesProTaskDO task1 = createTaskPojo(o -> o.setStatus(0));
MesProTaskDO task2 = createTaskPojo(o -> o.setStatus(1));
taskMapper.insert(task1);
taskMapper.insert(task2);
MesProTaskPageReqVO reqVO = new MesProTaskPageReqVO();
PageResult<MesProTaskDO> result = taskMapper.selectPage(reqVO);
assertEquals(2, result.getTotal());
}
// ==================== selectPage + checkFlag (MPJ LEFT JOIN) ====================
@Test
public void testSelectPage_checkFlagTrue() {
Long routeId = 10L;
Long processId1 = 100L;
Long processId2 = 200L;
MesProTaskDO task1 = createTaskPojo(o -> {
o.setRouteId(routeId);
o.setProcessId(processId1);
o.setStatus(0);
});
MesProTaskDO task2 = createTaskPojo(o -> {
o.setRouteId(routeId);
o.setProcessId(processId2);
o.setStatus(0);
});
taskMapper.insert(task1);
taskMapper.insert(task2);
routeProcessMapper.insert(randomPojo(MesProRouteProcessDO.class, o -> {
o.setRouteId(routeId);
o.setProcessId(processId1);
o.setCheckFlag(true);
}));
routeProcessMapper.insert(randomPojo(MesProRouteProcessDO.class, o -> {
o.setRouteId(routeId);
o.setProcessId(processId2);
o.setCheckFlag(false);
}));
// 查询checkFlag = true只返回 task1
MesProTaskPageReqVO reqVO = new MesProTaskPageReqVO();
reqVO.setCheckFlag(true);
PageResult<MesProTaskDO> result = taskMapper.selectPage(reqVO);
assertEquals(1, result.getTotal());
assertPojoEquals(task1, result.getList().get(0));
}
@Test
public void testSelectPage_checkFlagFalse() {
Long routeId = 20L;
Long processId1 = 300L;
Long processId2 = 400L;
MesProTaskDO task1 = createTaskPojo(o -> {
o.setRouteId(routeId);
o.setProcessId(processId1);
o.setStatus(0);
});
MesProTaskDO task2 = createTaskPojo(o -> {
o.setRouteId(routeId);
o.setProcessId(processId2);
o.setStatus(0);
});
taskMapper.insert(task1);
taskMapper.insert(task2);
routeProcessMapper.insert(randomPojo(MesProRouteProcessDO.class, o -> {
o.setRouteId(routeId);
o.setProcessId(processId1);
o.setCheckFlag(true);
}));
routeProcessMapper.insert(randomPojo(MesProRouteProcessDO.class, o -> {
o.setRouteId(routeId);
o.setProcessId(processId2);
o.setCheckFlag(false);
}));
// 查询checkFlag = false只返回 task2
MesProTaskPageReqVO reqVO = new MesProTaskPageReqVO();
reqVO.setCheckFlag(false);
PageResult<MesProTaskDO> result = taskMapper.selectPage(reqVO);
assertEquals(1, result.getTotal());
assertPojoEquals(task2, result.getList().get(0));
}
@Test
public void testSelectPage_checkFlagWithOtherFilter() {
Long routeId = 30L;
Long processId = 500L;
MesProTaskDO taskMatch = createTaskPojo(o -> {
o.setRouteId(routeId);
o.setProcessId(processId);
o.setCode("PT-MATCH-001");
o.setStatus(0);
});
MesProTaskDO taskNoMatch = createTaskPojo(o -> {
o.setRouteId(routeId);
o.setProcessId(processId);
o.setCode("OTHER-CODE");
o.setStatus(0);
});
taskMapper.insert(taskMatch);
taskMapper.insert(taskNoMatch);
routeProcessMapper.insert(randomPojo(MesProRouteProcessDO.class, o -> {
o.setRouteId(routeId);
o.setProcessId(processId);
o.setCheckFlag(true);
}));
// 查询checkFlag=true 且 code like 'PT-MATCH'
MesProTaskPageReqVO reqVO = new MesProTaskPageReqVO();
reqVO.setCheckFlag(true);
reqVO.setCode("PT-MATCH");
PageResult<MesProTaskDO> result = taskMapper.selectPage(reqVO);
assertEquals(1, result.getTotal());
assertPojoEquals(taskMatch, result.getList().get(0));
}
@Test
public void testSelectPage_checkFlagNull_noJoin() {
Long routeId = 40L;
Long processId = 600L;
MesProTaskDO task = createTaskPojo(o -> {
o.setRouteId(routeId);
o.setProcessId(processId);
o.setStatus(0);
});
taskMapper.insert(task);
MesProTaskPageReqVO reqVO = new MesProTaskPageReqVO();
PageResult<MesProTaskDO> result = taskMapper.selectPage(reqVO);
assertEquals(1, result.getTotal());
assertPojoEquals(task, result.getList().get(0));
}
}

View File

@@ -0,0 +1,220 @@
package cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDetailDO;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.List;
import java.util.function.Consumer;
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link MesWmProductSalesDetailMapper} 的单元测试
* <p>
* 覆盖selectListByLineId、selectListBySalesId、deleteByLineId、deleteBySalesId
*/
public class MesWmProductSalesDetailMapperTest extends BaseDbUnitTest {
@Resource
private MesWmProductSalesDetailMapper detailMapper;
// ==================== 辅助方法 ====================
/**
* 创建明细 DO固定 BigDecimal 精度为 2 位,避免 H2 decimal(14,2) 精度不匹配
*/
private MesWmProductSalesDetailDO createDetailPojo(Consumer<MesWmProductSalesDetailDO> consumer) {
return randomPojo(MesWmProductSalesDetailDO.class, o -> {
o.setQuantity(new BigDecimal("10.00"));
consumer.accept(o);
});
}
// ==================== selectListByLineId ====================
@Test
public void testSelectListByLineId_match() {
Long lineId = 100L;
MesWmProductSalesDetailDO match = createDetailPojo(o -> {
o.setLineId(lineId);
o.setSalesId(1L);
});
detailMapper.insert(match);
// 插入另一条不同 lineId 的记录,不应被返回
detailMapper.insert(cloneIgnoreId(match, o -> o.setLineId(999L)));
List<MesWmProductSalesDetailDO> result = detailMapper.selectListByLineId(lineId);
assertEquals(1, result.size());
assertPojoEquals(match, result.get(0));
}
@Test
public void testSelectListByLineId_multipleRows() {
Long lineId = 200L;
MesWmProductSalesDetailDO detail1 = createDetailPojo(o -> {
o.setLineId(lineId);
o.setSalesId(2L);
});
MesWmProductSalesDetailDO detail2 = createDetailPojo(o -> {
o.setLineId(lineId);
o.setSalesId(2L);
});
detailMapper.insert(detail1);
detailMapper.insert(detail2);
List<MesWmProductSalesDetailDO> result = detailMapper.selectListByLineId(lineId);
assertEquals(2, result.size());
}
@Test
public void testSelectListByLineId_noMatch() {
MesWmProductSalesDetailDO detail = createDetailPojo(o -> {
o.setLineId(300L);
o.setSalesId(3L);
});
detailMapper.insert(detail);
List<MesWmProductSalesDetailDO> result = detailMapper.selectListByLineId(999L);
assertTrue(result.isEmpty());
}
// ==================== selectListBySalesId ====================
@Test
public void testSelectListBySalesId_match() {
Long salesId = 10L;
MesWmProductSalesDetailDO match = createDetailPojo(o -> {
o.setSalesId(salesId);
o.setLineId(1L);
});
detailMapper.insert(match);
// 插入另一条不同 salesId 的记录
detailMapper.insert(cloneIgnoreId(match, o -> o.setSalesId(888L)));
List<MesWmProductSalesDetailDO> result = detailMapper.selectListBySalesId(salesId);
assertEquals(1, result.size());
assertPojoEquals(match, result.get(0));
}
@Test
public void testSelectListBySalesId_multipleRows() {
Long salesId = 20L;
MesWmProductSalesDetailDO detail1 = createDetailPojo(o -> {
o.setSalesId(salesId);
o.setLineId(11L);
});
MesWmProductSalesDetailDO detail2 = createDetailPojo(o -> {
o.setSalesId(salesId);
o.setLineId(12L);
});
detailMapper.insert(detail1);
detailMapper.insert(detail2);
List<MesWmProductSalesDetailDO> result = detailMapper.selectListBySalesId(salesId);
assertEquals(2, result.size());
}
@Test
public void testSelectListBySalesId_noMatch() {
MesWmProductSalesDetailDO detail = createDetailPojo(o -> {
o.setSalesId(30L);
o.setLineId(2L);
});
detailMapper.insert(detail);
List<MesWmProductSalesDetailDO> result = detailMapper.selectListBySalesId(9999L);
assertTrue(result.isEmpty());
}
// ==================== deleteByLineId ====================
@Test
public void testDeleteByLineId_deletesOnlyTargetLine() {
Long lineId = 400L;
MesWmProductSalesDetailDO toDelete = createDetailPojo(o -> {
o.setLineId(lineId);
o.setSalesId(4L);
});
MesWmProductSalesDetailDO toKeep = createDetailPojo(o -> {
o.setLineId(500L);
o.setSalesId(4L);
});
detailMapper.insert(toDelete);
detailMapper.insert(toKeep);
detailMapper.deleteByLineId(lineId);
// lineId=400 的记录已被逻辑删除
List<MesWmProductSalesDetailDO> deletedResult = detailMapper.selectListByLineId(lineId);
assertTrue(deletedResult.isEmpty(), "lineId=400 的记录应被删除");
// lineId=500 的记录保留
List<MesWmProductSalesDetailDO> keptResult = detailMapper.selectListByLineId(500L);
assertEquals(1, keptResult.size());
}
@Test
public void testDeleteByLineId_multipleRows() {
Long lineId = 600L;
detailMapper.insert(createDetailPojo(o -> { o.setLineId(lineId); o.setSalesId(5L); }));
detailMapper.insert(createDetailPojo(o -> { o.setLineId(lineId); o.setSalesId(5L); }));
detailMapper.deleteByLineId(lineId);
List<MesWmProductSalesDetailDO> result = detailMapper.selectListByLineId(lineId);
assertTrue(result.isEmpty(), "同 lineId 下的所有记录均应被删除");
}
// ==================== deleteBySalesId ====================
@Test
public void testDeleteBySalesId_deletesOnlyTargetSales() {
Long salesId = 700L;
MesWmProductSalesDetailDO toDelete = createDetailPojo(o -> {
o.setSalesId(salesId);
o.setLineId(6L);
});
MesWmProductSalesDetailDO toKeep = createDetailPojo(o -> {
o.setSalesId(800L);
o.setLineId(7L);
});
detailMapper.insert(toDelete);
detailMapper.insert(toKeep);
detailMapper.deleteBySalesId(salesId);
// salesId=700 的记录已被逻辑删除
List<MesWmProductSalesDetailDO> deletedResult = detailMapper.selectListBySalesId(salesId);
assertTrue(deletedResult.isEmpty(), "salesId=700 的记录应被删除");
// salesId=800 的记录保留
List<MesWmProductSalesDetailDO> keptResult = detailMapper.selectListBySalesId(800L);
assertEquals(1, keptResult.size());
}
@Test
public void testDeleteBySalesId_multipleRows() {
Long salesId = 900L;
detailMapper.insert(createDetailPojo(o -> { o.setSalesId(salesId); o.setLineId(8L); }));
detailMapper.insert(createDetailPojo(o -> { o.setSalesId(salesId); o.setLineId(9L); }));
detailMapper.insert(createDetailPojo(o -> { o.setSalesId(salesId); o.setLineId(10L); }));
detailMapper.deleteBySalesId(salesId);
List<MesWmProductSalesDetailDO> result = detailMapper.selectListBySalesId(salesId);
assertTrue(result.isEmpty(), "同 salesId 下的所有记录均应被删除");
}
}

View File

@@ -0,0 +1,119 @@
package cn.iocoder.yudao.module.mes.service.cal.plan;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.controller.admin.cal.plan.vo.MesCalPlanSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanDO;
import cn.iocoder.yudao.module.mes.dal.mysql.cal.plan.MesCalPlanMapper;
import cn.iocoder.yudao.module.mes.enums.cal.MesCalPlanStatusEnum;
import cn.iocoder.yudao.module.mes.enums.cal.MesCalShiftMethodEnum;
import cn.iocoder.yudao.module.mes.enums.cal.MesCalShiftTypeEnum;
import cn.iocoder.yudao.module.mes.service.cal.team.MesCalTeamShiftService;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import jakarta.annotation.Resource;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* {@link MesCalPlanServiceImpl} 的单元测试
*
* @author 芋道源码
*/
@Import(MesCalPlanServiceImpl.class)
public class MesCalPlanServiceImplTest extends BaseDbUnitTest {
@Resource
private MesCalPlanServiceImpl planService;
@Resource
private MesCalPlanMapper planMapper;
@MockitoBean
private MesCalPlanShiftService planShiftService;
@MockitoBean
private MesCalPlanTeamService planTeamService;
@MockitoBean
private MesCalTeamShiftService teamShiftService;
@Test
public void testCreatePlan_singleShift_addDefaultShift() {
// 准备参数
MesCalPlanSaveReqVO reqVO = new MesCalPlanSaveReqVO();
reqVO.setCode("PLAN-SINGLE-001");
reqVO.setName("单白班默认班次");
reqVO.setCalendarType(1);
reqVO.setStartDate(LocalDateTime.of(2026, 4, 1, 0, 0));
reqVO.setEndDate(LocalDateTime.of(2026, 4, 30, 0, 0));
reqVO.setShiftType(MesCalShiftTypeEnum.SINGLE.getType());
reqVO.setShiftMethod(MesCalShiftMethodEnum.DAY.getMethod());
reqVO.setShiftCount(1);
// 调用
Long planId = planService.createPlan(reqVO);
// 断言 1计划状态为草稿
MesCalPlanDO plan = planMapper.selectById(planId);
assertEquals(MesCalPlanStatusEnum.PREPARE.getStatus(), plan.getStatus());
// 断言 2自动生成默认班次
verify(planShiftService).addDefaultPlanShift(planId, MesCalShiftTypeEnum.SINGLE.getType());
}
@Test
public void testConfirmPlan_syncGenerateTeamShiftRecords() {
// mock 数据:插入一条草稿状态的排班计划
MesCalPlanDO plan = randomPojo(MesCalPlanDO.class, o -> {
o.setCalendarType(1);
o.setStartDate(LocalDateTime.of(2026, 4, 1, 0, 0));
o.setEndDate(LocalDateTime.of(2026, 4, 3, 0, 0));
o.setStatus(MesCalPlanStatusEnum.PREPARE.getStatus());
o.setShiftType(MesCalShiftTypeEnum.SINGLE.getType());
o.setShiftMethod(MesCalShiftMethodEnum.DAY.getMethod());
o.setShiftCount(1);
});
planMapper.insert(plan);
// mock 方法:班组数量满足要求
when(planTeamService.getPlanTeamCountByPlanId(plan.getId())).thenReturn(1L);
// 调用
planService.confirmPlan(plan.getId());
// 断言 1计划状态更新为已确认
MesCalPlanDO updatePlan = planMapper.selectById(plan.getId());
assertEquals(MesCalPlanStatusEnum.CONFIRMED.getStatus(), updatePlan.getStatus());
// 断言 2触发生成班组排班记录
verify(teamShiftService).generateTeamShiftRecords(plan.getId());
}
@Test
public void testDeletePlan_notCascadeDeleteTeamShift() {
// mock 数据:插入一条草稿状态的排班计划
MesCalPlanDO plan = randomPojo(MesCalPlanDO.class, o -> {
o.setCalendarType(1);
o.setStartDate(LocalDateTime.of(2026, 4, 1, 0, 0));
o.setEndDate(LocalDateTime.of(2026, 4, 3, 0, 0));
o.setStatus(MesCalPlanStatusEnum.PREPARE.getStatus());
o.setShiftType(MesCalShiftTypeEnum.SINGLE.getType());
o.setShiftMethod(MesCalShiftMethodEnum.DAY.getMethod());
o.setShiftCount(1);
});
planMapper.insert(plan);
// 调用
planService.deletePlan(plan.getId());
// 断言 1计划被删除
assertNull(planMapper.selectById(plan.getId()));
// 断言 2级联删除班次和班组关联
verify(planShiftService).deletePlanShiftByPlanId(plan.getId());
verify(planTeamService).deleteByPlanId(plan.getId());
// 断言 3不级联删除班组排班记录
verify(teamShiftService, never()).deleteByPlanId(plan.getId());
}
}

View File

@@ -0,0 +1,53 @@
package cn.iocoder.yudao.module.mes.service.cal.plan;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanShiftDO;
import cn.iocoder.yudao.module.mes.dal.mysql.cal.plan.MesCalPlanShiftMapper;
import cn.iocoder.yudao.module.mes.enums.cal.MesCalShiftTypeEnum;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import jakarta.annotation.Resource;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link MesCalPlanShiftServiceImpl} 的单元测试
*
* @author 芋道源码
*/
@Import(MesCalPlanShiftServiceImpl.class)
public class MesCalPlanShiftServiceImplTest extends BaseDbUnitTest {
@Resource
private MesCalPlanShiftServiceImpl planShiftService;
@Resource
private MesCalPlanShiftMapper planShiftMapper;
@MockitoBean
private MesCalPlanService planService;
@Test
public void testAddDefaultPlanShift_singleShift_use18Clock() {
// 准备参数
Long planId = 1001L;
// 调用
planShiftService.addDefaultPlanShift(planId, MesCalShiftTypeEnum.SINGLE.getType());
// 断言:生成 1 条默认班次(白班 08:00~18:00
List<MesCalPlanShiftDO> shifts = planShiftMapper.selectListByPlanId(planId);
assertEquals(1, shifts.size());
MesCalPlanShiftDO shift = shifts.get(0);
assertEquals(planId, shift.getPlanId());
assertEquals(1, shift.getSort());
assertEquals("白班", shift.getName());
assertEquals("08:00", shift.getStartTime());
assertEquals("18:00", shift.getEndTime());
}
}

View File

@@ -0,0 +1,108 @@
package cn.iocoder.yudao.module.mes.service.cal.team;
import cn.hutool.core.collection.ListUtil;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanShiftDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanTeamDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.cal.team.MesCalTeamShiftDO;
import cn.iocoder.yudao.module.mes.dal.mysql.cal.team.MesCalTeamShiftMapper;
import cn.iocoder.yudao.module.mes.enums.cal.MesCalShiftMethodEnum;
import cn.iocoder.yudao.module.mes.enums.cal.MesCalShiftTypeEnum;
import cn.iocoder.yudao.module.mes.service.cal.plan.MesCalPlanService;
import cn.iocoder.yudao.module.mes.service.cal.plan.MesCalPlanShiftService;
import cn.iocoder.yudao.module.mes.service.cal.plan.MesCalPlanTeamService;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import jakarta.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* {@link MesCalTeamShiftServiceImpl} 的单元测试
*
* @author 芋道源码
*/
@Import(MesCalTeamShiftServiceImpl.class)
public class MesCalTeamShiftServiceImplTest extends BaseDbUnitTest {
@Resource
private MesCalTeamShiftServiceImpl teamShiftService;
@Resource
private MesCalTeamShiftMapper teamShiftMapper;
@MockitoBean
private MesCalPlanService planService;
@MockitoBean
private MesCalPlanShiftService planShiftService;
@MockitoBean
private MesCalPlanTeamService planTeamService;
@Test
public void testGenerateTeamShiftRecords_threeShift_rotateThreeStates() {
// 准备参数
Long planId = 3001L;
// mock 方法排班计划三班倒、按天轮班、3 天)
MesCalPlanDO plan = MesCalPlanDO.builder()
.id(planId)
.code("PLAN-THREE-001")
.name("三班倒轮转")
.calendarType(1)
.startDate(LocalDateTime.of(2026, 4, 1, 0, 0))
.endDate(LocalDateTime.of(2026, 4, 3, 0, 0))
.shiftType(MesCalShiftTypeEnum.THREE.getType())
.shiftMethod(MesCalShiftMethodEnum.DAY.getMethod())
.shiftCount(1)
.build();
List<MesCalPlanShiftDO> shifts = ListUtil.of(
MesCalPlanShiftDO.builder().id(11L).planId(planId).sort(1).name("白班").startTime("08:00").endTime("16:00").build(),
MesCalPlanShiftDO.builder().id(12L).planId(planId).sort(2).name("中班").startTime("16:00").endTime("00:00").build(),
MesCalPlanShiftDO.builder().id(13L).planId(planId).sort(3).name("夜班").startTime("00:00").endTime("08:00").build()
);
List<MesCalPlanTeamDO> teams = ListUtil.of(
MesCalPlanTeamDO.builder().id(21L).planId(planId).teamId(101L).build(),
MesCalPlanTeamDO.builder().id(22L).planId(planId).teamId(102L).build(),
MesCalPlanTeamDO.builder().id(23L).planId(planId).teamId(103L).build()
);
when(planService.getPlan(planId)).thenReturn(plan);
when(planShiftService.getPlanShiftListByPlanId(planId)).thenReturn(shifts);
when(planTeamService.getPlanTeamListByPlanId(planId)).thenReturn(teams);
// 调用
teamShiftService.generateTeamShiftRecords(planId);
// 断言3 天 × 3 班次 = 9 条排班记录,班组按天轮转
List<MesCalTeamShiftDO> records = teamShiftMapper.selectListByPlanId(planId);
records.sort(Comparator.comparing(MesCalTeamShiftDO::getDay).thenComparing(MesCalTeamShiftDO::getSort));
assertEquals(9, records.size());
// 断言:第 1 天shiftIndex=0A 白班、B 中班、C 夜班
assertRecord(records.get(0), LocalDateTime.of(2026, 4, 1, 0, 0), 1, 101L, 11L);
assertRecord(records.get(1), LocalDateTime.of(2026, 4, 1, 0, 0), 2, 102L, 12L);
assertRecord(records.get(2), LocalDateTime.of(2026, 4, 1, 0, 0), 3, 103L, 13L);
// 断言:第 2 天shiftIndex=1C 白班、A 中班、B 夜班
assertRecord(records.get(3), LocalDateTime.of(2026, 4, 2, 0, 0), 1, 103L, 11L);
assertRecord(records.get(4), LocalDateTime.of(2026, 4, 2, 0, 0), 2, 101L, 12L);
assertRecord(records.get(5), LocalDateTime.of(2026, 4, 2, 0, 0), 3, 102L, 13L);
// 断言:第 3 天shiftIndex=2B 白班、C 中班、A 夜班
assertRecord(records.get(6), LocalDateTime.of(2026, 4, 3, 0, 0), 1, 102L, 11L);
assertRecord(records.get(7), LocalDateTime.of(2026, 4, 3, 0, 0), 2, 103L, 12L);
assertRecord(records.get(8), LocalDateTime.of(2026, 4, 3, 0, 0), 3, 101L, 13L);
}
private static void assertRecord(MesCalTeamShiftDO actual, LocalDateTime day, Integer sort,
Long teamId, Long shiftId) {
assertEquals(day, actual.getDay());
assertEquals(sort, actual.getSort());
assertEquals(teamId, actual.getTeamId());
assertEquals(shiftId, actual.getShiftId());
}
}

View File

@@ -0,0 +1,211 @@
package cn.iocoder.yudao.module.mes.service.md.autocode;
import cn.hutool.core.util.ReflectUtil;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodeRecordDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodeRuleDO;
import cn.iocoder.yudao.module.mes.dal.mysql.md.autocode.MesMdAutoCodeRecordMapper;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodePartTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.strategy.MesMdAutoCodeContext;
import cn.iocoder.yudao.module.mes.service.md.autocode.strategy.MesMdAutoCodePartStrategy;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* {@link MesMdAutoCodeRecordServiceImpl} 的单元测试
*
* @author 芋道源码
*/
@ExtendWith(MockitoExtension.class)
public class MesMdAutoCodeRecordServiceImplTest {
@InjectMocks
private MesMdAutoCodeRecordServiceImpl recordService;
@Mock
private MesMdAutoCodeRecordMapper recordMapper;
@Mock
private MesMdAutoCodeRuleService ruleService;
@Mock
private MesMdAutoCodePartService partService;
@Mock
private MesMdAutoCodePartStrategy fixedCharStrategy;
@Mock
private MesMdAutoCodePartStrategy serialNumberStrategy;
@Mock
private MesMdAutoCodePartStrategy inputCharStrategy;
@Mock
private MesMdAutoCodePartStrategy dateStrategy;
@BeforeEach
public void setUp() {
// 使用反射注入 strategyList
List<MesMdAutoCodePartStrategy> strategyList = Arrays.asList(
fixedCharStrategy, serialNumberStrategy, inputCharStrategy, dateStrategy
);
when(fixedCharStrategy.getType()).thenReturn(MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType());
when(serialNumberStrategy.getType()).thenReturn(MesMdAutoCodePartTypeEnum.SERIAL_NUMBER.getType());
when(inputCharStrategy.getType()).thenReturn(MesMdAutoCodePartTypeEnum.INPUT_CHAR.getType());
when(dateStrategy.getType()).thenReturn(MesMdAutoCodePartTypeEnum.DATE.getType());
ReflectUtil.setFieldValue(recordService, "strategyList", strategyList);
// 手动调用 init 方法初始化策略 map
recordService.init();
}
@Test
public void testGenerateAutoCode_fixedCharAndSerialNumber() {
// 准备参数
String ruleCode = "ITEM_CODE";
MesMdAutoCodeRuleDO rule = new MesMdAutoCodeRuleDO().setId(1L).setCode(ruleCode).setPadded(false);
MesMdAutoCodePartDO part1 = new MesMdAutoCodePartDO().setType(MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType())
.setFixCharacter("ITEM_").setLength(5).setSort(1);
MesMdAutoCodePartDO part2 = new MesMdAutoCodePartDO().setType(MesMdAutoCodePartTypeEnum.SERIAL_NUMBER.getType())
.setSerialStartNo(1).setSerialStep(1).setLength(4).setSort(2).setCycleFlag(false);
// mock
when(ruleService.getAutoCodeRuleByCode(ruleCode)).thenReturn(rule);
when(partService.getAutoCodePartListByRuleId(1L)).thenReturn(Arrays.asList(part1, part2));
when(fixedCharStrategy.generate(eq(part1), any(MesMdAutoCodeContext.class))).thenReturn("ITEM_");
when(serialNumberStrategy.generate(eq(part2), any(MesMdAutoCodeContext.class))).thenAnswer(invocation -> {
MesMdAutoCodeContext context = invocation.getArgument(1);
context.setSerialNo(1L);
return "0001";
});
when(recordMapper.selectByResult("ITEM_0001")).thenReturn(null);
// 调用
String result = recordService.generateAutoCode(ruleCode, null);
// 断言
assertEquals("ITEM_0001", result);
ArgumentCaptor<MesMdAutoCodeRecordDO> captor = ArgumentCaptor.forClass(MesMdAutoCodeRecordDO.class);
verify(recordMapper).insert(captor.capture());
MesMdAutoCodeRecordDO record = captor.getValue();
assertEquals(1L, record.getRuleId());
assertEquals("ITEM_0001", record.getResult());
assertEquals(1, record.getSerialNo());
}
@Test
public void testGenerateAutoCode_retryWhenResultExists() {
// 准备参数
String ruleCode = "DV_MACHINERY_CODE";
MesMdAutoCodeRuleDO rule = new MesMdAutoCodeRuleDO().setId(5L).setCode(ruleCode).setPadded(false);
MesMdAutoCodePartDO part1 = new MesMdAutoCodePartDO().setType(MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType())
.setFixCharacter("M").setLength(1).setSort(1);
MesMdAutoCodePartDO part2 = new MesMdAutoCodePartDO().setType(MesMdAutoCodePartTypeEnum.SERIAL_NUMBER.getType())
.setSerialStartNo(1).setSerialStep(1).setLength(5).setSort(2).setCycleFlag(false);
// mock
when(ruleService.getAutoCodeRuleByCode(ruleCode)).thenReturn(rule);
when(partService.getAutoCodePartListByRuleId(5L)).thenReturn(Arrays.asList(part1, part2));
when(fixedCharStrategy.generate(eq(part1), any(MesMdAutoCodeContext.class))).thenReturn("M");
long[] serialNoRef = {0L};
when(serialNumberStrategy.generate(eq(part2), any(MesMdAutoCodeContext.class))).thenAnswer(invocation -> {
MesMdAutoCodeContext context = invocation.getArgument(1);
long serialNo = ++serialNoRef[0];
context.setSerialNo(serialNo);
return String.format("%05d", serialNo);
});
when(recordMapper.selectByResult("M00001")).thenReturn(new MesMdAutoCodeRecordDO());
when(recordMapper.selectByResult("M00002")).thenReturn(new MesMdAutoCodeRecordDO());
when(recordMapper.selectByResult("M00003")).thenReturn(null);
// 调用
String result = recordService.generateAutoCode(ruleCode, null);
// 断言
assertEquals("M00003", result);
ArgumentCaptor<MesMdAutoCodeRecordDO> captor = ArgumentCaptor.forClass(MesMdAutoCodeRecordDO.class);
verify(recordMapper).insert(captor.capture());
MesMdAutoCodeRecordDO record = captor.getValue();
assertEquals(5L, record.getRuleId());
assertEquals("M00003", record.getResult());
assertEquals(3L, record.getSerialNo());
}
@Test
public void testGenerateAutoCode_withInputChar() {
// 准备参数
String ruleCode = "CLIENT_CODE";
String inputChar = "A";
MesMdAutoCodeRuleDO rule = new MesMdAutoCodeRuleDO().setId(2L).setCode(ruleCode).setPadded(false);
MesMdAutoCodePartDO part1 = new MesMdAutoCodePartDO().setType(MesMdAutoCodePartTypeEnum.INPUT_CHAR.getType())
.setLength(1).setSort(1);
MesMdAutoCodePartDO part2 = new MesMdAutoCodePartDO().setType(MesMdAutoCodePartTypeEnum.SERIAL_NUMBER.getType())
.setSerialStartNo(1).setSerialStep(1).setLength(4).setSort(2).setCycleFlag(false);
// mock
when(ruleService.getAutoCodeRuleByCode(ruleCode)).thenReturn(rule);
when(partService.getAutoCodePartListByRuleId(2L)).thenReturn(Arrays.asList(part1, part2));
when(inputCharStrategy.generate(eq(part1), any(MesMdAutoCodeContext.class))).thenReturn("A");
when(serialNumberStrategy.generate(eq(part2), any(MesMdAutoCodeContext.class))).thenReturn("0001");
when(recordMapper.selectByResult("A0001")).thenReturn(null);
// 调用
String result = recordService.generateAutoCode(ruleCode, inputChar);
// 断言
assertEquals("A0001", result);
ArgumentCaptor<MesMdAutoCodeRecordDO> captor = ArgumentCaptor.forClass(MesMdAutoCodeRecordDO.class);
verify(recordMapper).insert(captor.capture());
MesMdAutoCodeRecordDO record = captor.getValue();
assertEquals(inputChar, record.getInputChar());
assertEquals("A0001", record.getResult());
}
@Test
public void testGenerateAutoCode_withPadding() {
// 准备参数
String ruleCode = "PADDED_CODE";
MesMdAutoCodeRuleDO rule = new MesMdAutoCodeRuleDO().setId(3L).setCode(ruleCode)
.setPadded(true).setMaxLength(10).setPaddedChar("0").setPaddedMethod(1);
MesMdAutoCodePartDO part1 = new MesMdAutoCodePartDO().setType(MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType())
.setFixCharacter("IT").setLength(2).setSort(1);
// mock
when(ruleService.getAutoCodeRuleByCode(ruleCode)).thenReturn(rule);
when(partService.getAutoCodePartListByRuleId(3L)).thenReturn(Arrays.asList(part1));
when(fixedCharStrategy.generate(eq(part1), any(MesMdAutoCodeContext.class))).thenReturn("IT");
when(recordMapper.selectByResult("00000000IT")).thenReturn(null);
// 调用
String result = recordService.generateAutoCode(ruleCode, null);
// 断言IT 补齐到 10 位,左补 0
assertEquals("00000000IT", result);
verify(recordMapper).insert(any(MesMdAutoCodeRecordDO.class));
}
@Test
public void testGenerateAutoCode_withDatePart() {
// 准备参数
String ruleCode = "DATE_CODE";
MesMdAutoCodeRuleDO rule = new MesMdAutoCodeRuleDO().setId(4L).setCode(ruleCode).setPadded(false);
MesMdAutoCodePartDO part1 = new MesMdAutoCodePartDO().setType(MesMdAutoCodePartTypeEnum.DATE.getType())
.setDateFormat("yyyyMMdd").setLength(8).setSort(1);
MesMdAutoCodePartDO part2 = new MesMdAutoCodePartDO().setType(MesMdAutoCodePartTypeEnum.SERIAL_NUMBER.getType())
.setSerialStartNo(1).setSerialStep(1).setLength(4).setSort(2).setCycleFlag(false);
// mock
when(ruleService.getAutoCodeRuleByCode(ruleCode)).thenReturn(rule);
when(partService.getAutoCodePartListByRuleId(4L)).thenReturn(Arrays.asList(part1, part2));
when(dateStrategy.generate(eq(part1), any(MesMdAutoCodeContext.class))).thenReturn("20260304");
when(serialNumberStrategy.generate(eq(part2), any(MesMdAutoCodeContext.class))).thenReturn("0001");
when(recordMapper.selectByResult("202603040001")).thenReturn(null);
// 调用
String result = recordService.generateAutoCode(ruleCode, null);
// 断言:日期 8 位 + 流水号 4 位 = 12 位
assertEquals(12, result.length());
assertEquals("202603040001", result);
verify(recordMapper).insert(any(MesMdAutoCodeRecordDO.class));
}
}

View File

@@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.mes.service.md.autocode.strategy.impl;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO;
import cn.iocoder.yudao.module.mes.service.md.autocode.strategy.MesMdAutoCodeContext;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* {@link MesMdAutoCodeDatePartStrategy} 的单元测试
*
* @author 芋道源码
*/
public class MesMdAutoCodeDatePartStrategyTest {
private final MesMdAutoCodeDatePartStrategy strategy = new MesMdAutoCodeDatePartStrategy();
@Test
public void testGenerate_withDefaultFormat() {
// 准备参数
MesMdAutoCodePartDO part = new MesMdAutoCodePartDO().setDateFormat(null);
MesMdAutoCodeContext context = new MesMdAutoCodeContext();
// 调用
String result = strategy.generate(part, context);
// 断言:默认格式为 yyyyMMdd长度为 8
assertEquals(8, result.length());
assertTrue(result.matches("\\d{8}"));
}
@Test
public void testGenerate_withCustomFormat() {
// 准备参数
MesMdAutoCodePartDO part = new MesMdAutoCodePartDO().setDateFormat("yyyyMM");
MesMdAutoCodeContext context = new MesMdAutoCodeContext();
// 调用
String result = strategy.generate(part, context);
// 断言:格式为 yyyyMM长度为 6
assertEquals(6, result.length());
assertTrue(result.matches("\\d{6}"));
}
@Test
public void testGenerate_withTimeFormat() {
// 准备参数
MesMdAutoCodePartDO part = new MesMdAutoCodePartDO().setDateFormat("HHmmss");
MesMdAutoCodeContext context = new MesMdAutoCodeContext();
// 调用
String result = strategy.generate(part, context);
// 断言:格式为 HHmmss长度为 6
assertEquals(6, result.length());
assertTrue(result.matches("\\d{6}"));
}
}

View File

@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.mes.service.md.autocode.strategy.impl;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO;
import cn.iocoder.yudao.module.mes.service.md.autocode.strategy.MesMdAutoCodeContext;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* {@link MesMdAutoCodeFixedCharPartStrategy} 的单元测试
*
* @author 芋道源码
*/
public class MesMdAutoCodeFixedCharPartStrategyTest {
private final MesMdAutoCodeFixedCharPartStrategy strategy = new MesMdAutoCodeFixedCharPartStrategy();
@Test
public void testGenerate_withFixedChar() {
// 准备参数
MesMdAutoCodePartDO part = new MesMdAutoCodePartDO().setFixCharacter("ITEM_");
MesMdAutoCodeContext context = new MesMdAutoCodeContext();
// 调用
String result = strategy.generate(part, context);
// 断言
assertEquals("ITEM_", result);
}
@Test
public void testGenerate_withEmptyFixedChar() {
// 准备参数
MesMdAutoCodePartDO part = new MesMdAutoCodePartDO().setFixCharacter("");
MesMdAutoCodeContext context = new MesMdAutoCodeContext();
// 调用
String result = strategy.generate(part, context);
// 断言
assertEquals("", result);
}
@Test
public void testGenerate_withNullFixedChar() {
// 准备参数
MesMdAutoCodePartDO part = new MesMdAutoCodePartDO().setFixCharacter(null);
MesMdAutoCodeContext context = new MesMdAutoCodeContext();
// 调用
String result = strategy.generate(part, context);
// 断言
assertEquals("", result);
}
}

View File

@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.mes.service.md.autocode.strategy.impl;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO;
import cn.iocoder.yudao.module.mes.service.md.autocode.strategy.MesMdAutoCodeContext;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* {@link MesMdAutoCodeInputCharPartStrategy} 的单元测试
*
* @author 芋道源码
*/
public class MesMdAutoCodeInputCharPartStrategyTest {
private final MesMdAutoCodeInputCharPartStrategy strategy = new MesMdAutoCodeInputCharPartStrategy();
@Test
public void testGenerate_withInputChar() {
// 准备参数
MesMdAutoCodePartDO part = new MesMdAutoCodePartDO();
MesMdAutoCodeContext context = new MesMdAutoCodeContext().setInputChar("ABC");
// 调用
String result = strategy.generate(part, context);
// 断言
assertEquals("ABC", result);
}
@Test
public void testGenerate_withEmptyInputChar() {
// 准备参数
MesMdAutoCodePartDO part = new MesMdAutoCodePartDO();
MesMdAutoCodeContext context = new MesMdAutoCodeContext().setInputChar("");
// 调用
String result = strategy.generate(part, context);
// 断言
assertEquals("", result);
}
@Test
public void testGenerate_withNullInputChar() {
// 准备参数
MesMdAutoCodePartDO part = new MesMdAutoCodePartDO();
MesMdAutoCodeContext context = new MesMdAutoCodeContext().setInputChar(null);
// 调用
String result = strategy.generate(part, context);
// 断言
assertEquals("", result);
}
}

View File

@@ -0,0 +1,104 @@
package cn.iocoder.yudao.module.mes.service.md.autocode.strategy.impl;
import cn.iocoder.yudao.framework.test.core.ut.BaseRedisUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodeRuleDO;
import cn.iocoder.yudao.module.mes.dal.redis.md.autocode.MesMdAutoCodeRedisDAO;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeCycleMethodEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.strategy.MesMdAutoCodeContext;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link MesMdAutoCodeSerialNumberPartStrategy} 的单元测试
*
* @author 芋道源码
*/
@Import({MesMdAutoCodeSerialNumberPartStrategy.class, MesMdAutoCodeRedisDAO.class})
public class MesMdAutoCodeSerialNumberPartStrategyTest extends BaseRedisUnitTest {
@Resource
private MesMdAutoCodeSerialNumberPartStrategy strategy;
@Test
public void testGenerate_noCycle() {
// 准备参数
MesMdAutoCodePartDO part = new MesMdAutoCodePartDO().setLength(4).setSerialStartNo(1).setSerialStep(1).setCycleFlag(false);
MesMdAutoCodeRuleDO rule = new MesMdAutoCodeRuleDO().setId(1L);
MesMdAutoCodeContext context = new MesMdAutoCodeContext().setRule(rule);
// 调用
String result = strategy.generate(part, context);
// 断言
assertEquals("0001", result);
assertEquals(1, context.getSerialNo());
}
@Test
public void testGenerate_withCycleByDay() {
// 准备参数
MesMdAutoCodePartDO part = new MesMdAutoCodePartDO().setLength(4).setSerialStartNo(1).setSerialStep(1)
.setCycleFlag(true).setCycleMethod(MesMdAutoCodeCycleMethodEnum.DAY.getMethod());
MesMdAutoCodeRuleDO rule = new MesMdAutoCodeRuleDO().setId(2L);
MesMdAutoCodeContext context = new MesMdAutoCodeContext().setRule(rule);
// 调用
String result = strategy.generate(part, context);
// 断言
assertEquals("0001", result);
assertEquals(1, context.getSerialNo());
}
@Test
public void testGenerate_withCycleByInputChar() {
// 准备参数
MesMdAutoCodePartDO part = new MesMdAutoCodePartDO().setLength(4).setSerialStartNo(1).setSerialStep(1)
.setCycleFlag(true).setCycleMethod(MesMdAutoCodeCycleMethodEnum.INPUT_CHAR.getMethod());
MesMdAutoCodeRuleDO rule = new MesMdAutoCodeRuleDO().setId(3L);
MesMdAutoCodeContext context = new MesMdAutoCodeContext().setRule(rule).setInputChar("A");
// 调用
String result = strategy.generate(part, context);
// 断言
assertEquals("0001", result);
assertEquals(1, context.getSerialNo());
}
@Test
public void testGenerate_withStep() {
// 准备参数
MesMdAutoCodePartDO part = new MesMdAutoCodePartDO().setLength(4).setSerialStartNo(1).setSerialStep(5).setCycleFlag(false);
MesMdAutoCodeRuleDO rule = new MesMdAutoCodeRuleDO().setId(4L);
MesMdAutoCodeContext context1 = new MesMdAutoCodeContext().setRule(rule);
MesMdAutoCodeContext context2 = new MesMdAutoCodeContext().setRule(rule);
// 调用
String result1 = strategy.generate(part, context1);
String result2 = strategy.generate(part, context2);
// 断言
assertEquals("0001", result1);
assertEquals(1, context1.getSerialNo());
assertEquals("0006", result2);
assertEquals(6, context2.getSerialNo());
}
@Test
public void testGenerate_multipleCallsIncrement() {
// 准备参数
MesMdAutoCodePartDO part = new MesMdAutoCodePartDO().setLength(4).setSerialStartNo(1).setSerialStep(1).setCycleFlag(false);
MesMdAutoCodeRuleDO rule = new MesMdAutoCodeRuleDO().setId(5L);
MesMdAutoCodeContext context1 = new MesMdAutoCodeContext().setRule(rule);
MesMdAutoCodeContext context2 = new MesMdAutoCodeContext().setRule(rule);
// 调用
String result1 = strategy.generate(part, context1);
String result2 = strategy.generate(part, context2);
// 断言
assertEquals("0001", result1);
assertEquals("0002", result2);
}
}

View File

@@ -0,0 +1,397 @@
package cn.iocoder.yudao.module.mes.service.pro.feedback;
import cn.hutool.core.collection.ListUtil;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.wm.itemconsume.MesWmItemConsumeService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.math.BigDecimal;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* {@link MesProFeedbackServiceImpl} 的单元测试
*
* @author 芋道源码
*/
@Import(MesProFeedbackServiceImpl.class)
public class MesProFeedbackServiceImplTest extends BaseDbUnitTest {
@Resource
private MesProFeedbackServiceImpl feedbackService;
@Resource
private MesProFeedbackMapper feedbackMapper;
@MockitoBean
private MesProWorkOrderService workOrderService;
@MockitoBean
private MesProRouteProcessService routeProcessService;
@MockitoBean
private MesMdWorkstationService workstationService;
@MockitoBean
private MesProTaskService taskService;
@MockitoBean
private MesWmItemConsumeService itemConsumeService;
@MockitoBean
private MesWmProductProduceService productProduceService;
@MockitoBean
private MesWmProductProduceLineService produceLineService;
@Test
public void testUpdateProFeedbackWhenIpqcFinish_success_withUnqualified() {
// 准备数据:插入一条待检验状态的报工单
Long taskId = randomLongId();
Long workOrderId = randomLongId();
Long sourceLineId = randomLongId();
MesProFeedbackDO feedback = randomPojo(MesProFeedbackDO.class, o -> {
o.setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus());
o.setTaskId(taskId);
o.setWorkOrderId(workOrderId);
o.setFeedbackQuantity(BigDecimal.valueOf(100));
});
feedbackMapper.insert(feedback);
// mock: 产出行(用于 updateTaskAndWorkOrderByFeedback 内的数量聚合)
MesWmProductProduceLineDO qualifiedLine = MesWmProductProduceLineDO.builder()
.quantity(BigDecimal.valueOf(80))
.qualityStatus(MesWmQualityStatusEnum.PASS.getStatus())
.build();
MesWmProductProduceLineDO unqualifiedLine = MesWmProductProduceLineDO.builder()
.quantity(BigDecimal.valueOf(20))
.qualityStatus(MesWmQualityStatusEnum.FAIL.getStatus())
.build();
when(produceLineService.getProductProduceLineListByFeedbackId(feedback.getId()))
.thenReturn(ListUtil.of(qualifiedLine, unqualifiedLine));
// 调用
BigDecimal qualifiedQty = BigDecimal.valueOf(80);
BigDecimal unqualifiedQty = BigDecimal.valueOf(20);
BigDecimal laborScrapQty = BigDecimal.valueOf(5);
BigDecimal materialScrapQty = BigDecimal.valueOf(10);
BigDecimal otherScrapQty = BigDecimal.valueOf(5);
feedbackService.updateProFeedbackWhenIpqcFinish(feedback.getId(), sourceLineId,
qualifiedQty, unqualifiedQty, laborScrapQty, materialScrapQty, otherScrapQty);
// 断言 1调用了 splitPendingAndFinishProduce
verify(productProduceService).splitPendingAndFinishProduce(
eq(feedback.getId()), eq(sourceLineId), eq(qualifiedQty), eq(unqualifiedQty));
// 断言 2报工单状态更新为已完成
MesProFeedbackDO updatedFeedback = feedbackMapper.selectById(feedback.getId());
assertEquals(MesProFeedbackStatusEnum.FINISHED.getStatus(), updatedFeedback.getStatus());
// 断言 3数量回写正确
assertEquals(0, qualifiedQty.compareTo(updatedFeedback.getQualifiedQuantity()));
assertEquals(0, unqualifiedQty.compareTo(updatedFeedback.getUnqualifiedQuantity()));
assertEquals(0, BigDecimal.ZERO.compareTo(updatedFeedback.getUncheckQuantity()));
assertEquals(0, laborScrapQty.compareTo(updatedFeedback.getLaborScrapQuantity()));
assertEquals(0, materialScrapQty.compareTo(updatedFeedback.getMaterialScrapQuantity()));
assertEquals(0, otherScrapQty.compareTo(updatedFeedback.getOtherScrapQuantity()));
// 断言 4更新了任务和工单的已生产数量
// 注意:数量经过 DB 存取后 scale 可能变化,用 any() 匹配
verify(taskService).updateProducedQuantity(eq(taskId),
any(BigDecimal.class), any(BigDecimal.class), any(BigDecimal.class));
verify(workOrderService).updateProducedQuantity(eq(workOrderId),
any(BigDecimal.class));
}
@Test
public void testUpdateProFeedbackWhenIpqcFinish_success_allQualified() {
// 准备数据:全部合格
Long taskId = randomLongId();
Long workOrderId = randomLongId();
MesProFeedbackDO feedback = randomPojo(MesProFeedbackDO.class, o -> {
o.setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus());
o.setTaskId(taskId);
o.setWorkOrderId(workOrderId);
o.setFeedbackQuantity(BigDecimal.valueOf(50));
});
feedbackMapper.insert(feedback);
// mock: 产出行(全部合格)
MesWmProductProduceLineDO qualifiedLine = MesWmProductProduceLineDO.builder()
.quantity(BigDecimal.valueOf(50))
.qualityStatus(MesWmQualityStatusEnum.PASS.getStatus())
.build();
when(produceLineService.getProductProduceLineListByFeedbackId(feedback.getId()))
.thenReturn(ListUtil.of(qualifiedLine));
// 调用
feedbackService.updateProFeedbackWhenIpqcFinish(feedback.getId(), randomLongId(),
BigDecimal.valueOf(50), BigDecimal.ZERO,
BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO);
// 断言 1报工单状态为已完成
MesProFeedbackDO updatedFeedback = feedbackMapper.selectById(feedback.getId());
assertEquals(MesProFeedbackStatusEnum.FINISHED.getStatus(), updatedFeedback.getStatus());
// 断言 2合格品数量正确不合格品和废品为 0
assertEquals(0, BigDecimal.valueOf(50).compareTo(updatedFeedback.getQualifiedQuantity()));
assertEquals(0, BigDecimal.ZERO.compareTo(updatedFeedback.getUnqualifiedQuantity()));
assertEquals(0, BigDecimal.ZERO.compareTo(updatedFeedback.getLaborScrapQuantity()));
}
@Test
public void testUpdateProFeedbackWhenIpqcFinish_feedbackNotExists() {
// 调用不存在的 feedbackId应该抛异常
Long feedbackId = randomLongId();
assertThrows(Exception.class, () ->
feedbackService.updateProFeedbackWhenIpqcFinish(feedbackId, randomLongId(),
BigDecimal.TEN, BigDecimal.ZERO,
BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO));
}
@Test
public void testUpdateProFeedbackWhenIpqcFinish_feedbackNotUncheck() {
// 准备数据:草稿状态(不是 UNCHECK应该报错
MesProFeedbackDO feedback = randomPojo(MesProFeedbackDO.class, o -> {
o.setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus());
});
feedbackMapper.insert(feedback);
// 调用,应该抛异常
assertThrows(Exception.class, () ->
feedbackService.updateProFeedbackWhenIpqcFinish(feedback.getId(), randomLongId(),
BigDecimal.TEN, BigDecimal.ZERO,
BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO));
// 断言:不应该调用任何产出单方法
verify(productProduceService, never()).splitPendingAndFinishProduce(anyLong(), anyLong(), any(), any());
}
@Test
public void testUpdateProFeedbackWhenIpqcFinish_feedbackAlreadyFinished() {
// 准备数据:已完成状态,不能再次完成
MesProFeedbackDO feedback = randomPojo(MesProFeedbackDO.class, o -> {
o.setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus());
});
feedbackMapper.insert(feedback);
// 调用,应该抛异常
assertThrows(Exception.class, () ->
feedbackService.updateProFeedbackWhenIpqcFinish(feedback.getId(), randomLongId(),
BigDecimal.TEN, BigDecimal.ZERO,
BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO));
}
// ==================== approveFeedback 测试 ====================
@Test
public void testApproveFeedback_keyNonCheck_success() {
// 准备数据:关键非质检工序,常规审批
Long routeId = randomLongId();
Long processId = randomLongId();
MesProFeedbackDO feedback = randomPojo(MesProFeedbackDO.class, o -> {
o.setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus());
o.setFeedbackQuantity(BigDecimal.valueOf(100));
o.setQualifiedQuantity(BigDecimal.valueOf(80));
o.setUnqualifiedQuantity(BigDecimal.valueOf(20));
o.setUncheckQuantity(BigDecimal.ZERO);
o.setRouteId(routeId);
o.setProcessId(processId);
});
feedbackMapper.insert(feedback);
// mock: 工序配置 key=true, check=false
MesProRouteProcessDO routeProcess = MesProRouteProcessDO.builder()
.routeId(routeId).processId(processId)
.keyFlag(true).checkFlag(false).build();
when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, processId))
.thenReturn(routeProcess);
// mock: 产品产出单
MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class);
when(productProduceService.generateProductProduce(any(), eq(false))).thenReturn(produce);
// mock: 产出行(用于 updateTaskAndWorkOrderByFeedback
MesWmProductProduceLineDO qualifiedLine = MesWmProductProduceLineDO.builder()
.quantity(BigDecimal.valueOf(80))
.qualityStatus(MesWmQualityStatusEnum.PASS.getStatus()).build();
when(produceLineService.getProductProduceLineListByFeedbackId(feedback.getId()))
.thenReturn(ListUtil.of(qualifiedLine));
// 调用
boolean result = feedbackService.approveFeedback(feedback.getId());
// 断言 1返回 true已完成
assertTrue(result);
// 断言 2状态为已完成uncheckQuantity 清零
MesProFeedbackDO updated = feedbackMapper.selectById(feedback.getId());
assertEquals(MesProFeedbackStatusEnum.FINISHED.getStatus(), updated.getStatus());
assertEquals(0, BigDecimal.ZERO.compareTo(updated.getUncheckQuantity()));
// 断言 3调用了产出单生成 + 入库 + 任务/工单更新
verify(productProduceService).generateProductProduce(any(), eq(false));
verify(productProduceService).finishProductProduce(produce.getId());
verify(taskService).updateProducedQuantity(eq(feedback.getTaskId()),
any(BigDecimal.class), any(BigDecimal.class), any(BigDecimal.class));
verify(workOrderService).updateProducedQuantity(eq(feedback.getWorkOrderId()),
any(BigDecimal.class));
}
@Test
public void testApproveFeedback_keyCheck_enterUncheck() {
// 准备数据:关键质检工序,应进入待检验
Long routeId = randomLongId();
Long processId = randomLongId();
MesProFeedbackDO feedback = randomPojo(MesProFeedbackDO.class, o -> {
o.setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus());
o.setFeedbackQuantity(BigDecimal.valueOf(50));
o.setUncheckQuantity(BigDecimal.valueOf(50)); // 质检工序 uncheckQuantity > 0
o.setRouteId(routeId);
o.setProcessId(processId);
});
feedbackMapper.insert(feedback);
// mock: key=true, check=true
MesProRouteProcessDO routeProcess = MesProRouteProcessDO.builder()
.routeId(routeId).processId(processId)
.keyFlag(true).checkFlag(true).build();
when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, processId))
.thenReturn(routeProcess);
// 调用
boolean result = feedbackService.approveFeedback(feedback.getId());
// 断言 1返回 false待检验
assertFalse(result);
// 断言 2状态为待检验
MesProFeedbackDO updated = feedbackMapper.selectById(feedback.getId());
assertEquals(MesProFeedbackStatusEnum.UNCHECK.getStatus(), updated.getStatus());
// 断言 3生成了待检产出单但没有 finishProductProduce
verify(productProduceService).generateProductProduce(any(), eq(true));
verify(productProduceService, never()).finishProductProduce(anyLong());
// 断言 4没有更新任务/工单数量(等 IPQC 回调)
verify(taskService, never()).updateProducedQuantity(anyLong(), any(), any(), any());
verify(workOrderService, never()).updateProducedQuantity(anyLong(), any());
}
@Test
public void testApproveFeedback_nonKey_directFinish() {
// 准备数据:非关键工序,直接完结
Long routeId = randomLongId();
Long processId = randomLongId();
MesProFeedbackDO feedback = randomPojo(MesProFeedbackDO.class, o -> {
o.setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus());
o.setFeedbackQuantity(BigDecimal.valueOf(30));
o.setUncheckQuantity(BigDecimal.ZERO);
o.setRouteId(routeId);
o.setProcessId(processId);
});
feedbackMapper.insert(feedback);
// mock: key=false, check=false
MesProRouteProcessDO routeProcess = MesProRouteProcessDO.builder()
.routeId(routeId).processId(processId)
.keyFlag(false).checkFlag(false).build();
when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, processId))
.thenReturn(routeProcess);
// 调用
boolean result = feedbackService.approveFeedback(feedback.getId());
// 断言 1返回 true已完成
assertTrue(result);
// 断言 2状态为已完成
MesProFeedbackDO updated = feedbackMapper.selectById(feedback.getId());
assertEquals(MesProFeedbackStatusEnum.FINISHED.getStatus(), updated.getStatus());
// 断言 3不生成产出单不更新任务/工单
verify(productProduceService, never()).generateProductProduce(any(), anyBoolean());
verify(taskService, never()).updateProducedQuantity(anyLong(), any(), any(), any());
}
@Test
public void testApproveFeedback_nonCheck_uncheckQuantityReject() {
// 准备数据:非质检工序,但 uncheckQuantity > 0异常数据应被拦截
Long routeId = randomLongId();
Long processId = randomLongId();
MesProFeedbackDO feedback = randomPojo(MesProFeedbackDO.class, o -> {
o.setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus());
o.setFeedbackQuantity(BigDecimal.valueOf(10));
o.setUncheckQuantity(BigDecimal.valueOf(10)); // 非质检工序不应有待检数量
o.setRouteId(routeId);
o.setProcessId(processId);
});
feedbackMapper.insert(feedback);
// mock: key=true, check=false非质检
MesProRouteProcessDO routeProcess = MesProRouteProcessDO.builder()
.routeId(routeId).processId(processId)
.keyFlag(true).checkFlag(false).build();
when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, processId))
.thenReturn(routeProcess);
// 调用,应该抛异常
assertThrows(Exception.class, () ->
feedbackService.approveFeedback(feedback.getId()));
// 断言:不应该执行任何后续操作
verify(productProduceService, never()).generateProductProduce(any(), anyBoolean());
verify(itemConsumeService, never()).generateItemConsume(any());
}
@Test
public void testApproveFeedback_nonKeyCheck_directFinishAndCleanUncheck() {
// 准备数据:非关键 + 质检工序(!key+checkuncheckQuantity > 0
// 应放行checkFlag=true 不拦截 uncheckQuantity直接完结并清零 uncheckQuantity
Long routeId = randomLongId();
Long processId = randomLongId();
MesProFeedbackDO feedback = randomPojo(MesProFeedbackDO.class, o -> {
o.setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus());
o.setFeedbackQuantity(BigDecimal.valueOf(20));
o.setUncheckQuantity(BigDecimal.valueOf(20));
o.setRouteId(routeId);
o.setProcessId(processId);
});
feedbackMapper.insert(feedback);
// mock: key=false, check=true
MesProRouteProcessDO routeProcess = MesProRouteProcessDO.builder()
.routeId(routeId).processId(processId)
.keyFlag(false).checkFlag(true).build();
when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, processId))
.thenReturn(routeProcess);
// 调用
boolean result = feedbackService.approveFeedback(feedback.getId());
// 断言 1返回 true直接完成不走 UNCHECK
assertTrue(result);
// 断言 2状态为已完成uncheckQuantity 被清零
MesProFeedbackDO updated = feedbackMapper.selectById(feedback.getId());
assertEquals(MesProFeedbackStatusEnum.FINISHED.getStatus(), updated.getStatus());
assertEquals(0, BigDecimal.ZERO.compareTo(updated.getUncheckQuantity()));
// 断言 3非关键工序不生成产出单
verify(productProduceService, never()).generateProductProduce(any(), anyBoolean());
}
}

View File

@@ -0,0 +1,288 @@
package cn.iocoder.yudao.module.mes.service.qc.indicatorresult;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.controller.admin.qc.indicatorresult.vo.MesQcIndicatorResultPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.indicatorresult.vo.MesQcIndicatorResultSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.iqc.MesQcIqcDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.indicatorresult.MesQcIndicatorResultMapper;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcResultValueTypeEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
import cn.iocoder.yudao.module.mes.service.qc.indicator.MesQcIndicatorService;
import cn.iocoder.yudao.module.mes.service.qc.ipqc.MesQcIpqcService;
import cn.iocoder.yudao.module.mes.service.qc.iqc.MesQcIqcService;
import cn.iocoder.yudao.module.mes.service.qc.oqc.MesQcOqcService;
import cn.iocoder.yudao.module.mes.service.qc.rqc.MesQcRqcService;
import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.Mockito.*;
/**
* {@link MesQcIndicatorResultServiceImpl} 的单元测试
*
* @author 芋道源码
*/
@Import(MesQcIndicatorResultServiceImpl.class)
public class MesQcIndicatorResultServiceImplTest extends BaseDbUnitTest {
@Resource
private MesQcIndicatorResultServiceImpl indicatorResultService;
@Resource
private MesQcIndicatorResultMapper resultMapper;
@MockitoBean
private MesQcIndicatorResultDetailService resultDetailService;
@MockitoBean
private MesQcIndicatorService indicatorService;
@MockitoBean
private MesQcIqcService iqcService;
@MockitoBean
private MesQcIpqcService ipqcService;
@MockitoBean
private MesQcOqcService oqcService;
@MockitoBean
private MesQcRqcService rqcService;
@MockitoBean
private DictDataApi dictDataApi;
@Test
public void testCreateIndicatorResult_success() {
// 准备参数
MesQcIndicatorResultSaveReqVO reqVO = buildReqVO("plain text value", MesQcResultValueTypeEnum.TEXT.getType());
// mock 方法
mockIqcAndIndicator(reqVO, MesQcResultValueTypeEnum.TEXT.getType());
// 调用
Long resultId = indicatorResultService.createIndicatorResult(reqVO);
// 断言
assertNotNull(resultId);
MesQcIndicatorResultDO dbResult = resultMapper.selectById(resultId);
assertNotNull(dbResult);
assertEquals(reqVO.getCode(), dbResult.getCode());
assertEquals(reqVO.getQcId(), dbResult.getQcId());
assertEquals(reqVO.getQcType(), dbResult.getQcType());
assertEquals(100L, dbResult.getItemId()); // 从 mock IQC 获取
verify(resultDetailService).createDetailList(anyList());
}
@Test
public void testCreateIndicatorResult_fileValueInvalid() {
// 准备参数
MesQcIndicatorResultSaveReqVO reqVO = buildReqVO("not-a-url", MesQcResultValueTypeEnum.FILE.getType());
// mock 方法
mockIqcAndIndicator(reqVO, MesQcResultValueTypeEnum.FILE.getType());
// 调用,并断言异常
assertServiceException(() -> indicatorResultService.createIndicatorResult(reqVO),
QC_RESULT_VALUE_FORMAT_INVALID, "检测项[检测项A]要求文件 URL实际值=not-a-url");
// 断言:未入库
assertEquals(0, resultMapper.selectCount());
verify(resultDetailService, never()).createDetailList(anyList());
}
@Test
public void testUpdateIndicatorResult_success() {
// mock 数据
MesQcIndicatorResultDO dbResult = randomPojo(MesQcIndicatorResultDO.class, o -> {
o.setQcType(MesQcTypeEnum.IQC.getType());
});
resultMapper.insert(dbResult);
// 准备参数
MesQcIndicatorResultSaveReqVO reqVO = buildReqVO("updated text", MesQcResultValueTypeEnum.TEXT.getType());
reqVO.setId(dbResult.getId());
// mock 方法
MesQcIndicatorDO indicator = new MesQcIndicatorDO();
indicator.setId(reqVO.getItems().get(0).getIndicatorId());
indicator.setName("检测项A");
indicator.setResultType(MesQcResultValueTypeEnum.TEXT.getType());
when(indicatorService.validateIndicatorListExists(anySet()))
.thenReturn(MapUtil.of(indicator.getId(), indicator));
// 调用
indicatorResultService.updateIndicatorResult(reqVO);
// 断言
MesQcIndicatorResultDO updatedResult = resultMapper.selectById(dbResult.getId());
assertEquals(reqVO.getCode(), updatedResult.getCode());
// qcId/qcType/itemId 不允许改挂,应保持原值
assertEquals(dbResult.getQcId(), updatedResult.getQcId());
assertEquals(dbResult.getQcType(), updatedResult.getQcType());
assertEquals(dbResult.getItemId(), updatedResult.getItemId());
verify(resultDetailService).createOrUpdateDetailList(anyList());
}
@Test
public void testUpdateIndicatorResult_notExists() {
// 准备参数
MesQcIndicatorResultSaveReqVO reqVO = buildReqVO("text", MesQcResultValueTypeEnum.TEXT.getType());
reqVO.setId(randomLongId());
// 调用,并断言异常
assertServiceException(() -> indicatorResultService.updateIndicatorResult(reqVO),
QC_RESULT_NOT_EXISTS);
}
@Test
public void testDeleteIndicatorResult_success() {
// mock 数据
MesQcIndicatorResultDO dbResult = randomPojo(MesQcIndicatorResultDO.class);
resultMapper.insert(dbResult);
// 调用
indicatorResultService.deleteIndicatorResult(dbResult.getId());
// 断言
assertNull(resultMapper.selectById(dbResult.getId()));
verify(resultDetailService).deleteDetailByResultId(dbResult.getId());
}
@Test
public void testDeleteIndicatorResult_notExists() {
// 调用,并断言异常
assertServiceException(() -> indicatorResultService.deleteIndicatorResult(randomLongId()),
QC_RESULT_NOT_EXISTS);
}
@Test
public void testGetIndicatorResult() {
// mock 数据
MesQcIndicatorResultDO dbResult = randomPojo(MesQcIndicatorResultDO.class);
resultMapper.insert(dbResult);
// 调用
MesQcIndicatorResultDO result = indicatorResultService.getIndicatorResult(dbResult.getId());
// 断言
assertPojoEquals(dbResult, result);
}
@Test
public void testGetIndicatorResultPage() {
// mock 数据
MesQcIndicatorResultDO dbResult = randomPojo(MesQcIndicatorResultDO.class, o -> {
o.setQcId(1L);
o.setQcType(MesQcTypeEnum.IQC.getType());
o.setCode("SPL-001");
o.setItemId(100L);
});
resultMapper.insert(dbResult);
// 测试 qcId 不匹配
resultMapper.insert(cloneIgnoreId(dbResult, o -> o.setQcId(2L)));
// 测试 qcType 不匹配
resultMapper.insert(cloneIgnoreId(dbResult, o -> o.setQcType(MesQcTypeEnum.IPQC.getType())));
// 测试 code 不匹配
resultMapper.insert(cloneIgnoreId(dbResult, o -> o.setCode("SPL-999")));
// 测试 itemId 不匹配
resultMapper.insert(cloneIgnoreId(dbResult, o -> o.setItemId(999L)));
// 准备参数
MesQcIndicatorResultPageReqVO pageReqVO = new MesQcIndicatorResultPageReqVO();
pageReqVO.setQcId(1L);
pageReqVO.setQcType(MesQcTypeEnum.IQC.getType());
pageReqVO.setCode("SPL-001");
pageReqVO.setItemId(100L);
// 调用
PageResult<MesQcIndicatorResultDO> pageResult = indicatorResultService.getIndicatorResultPage(pageReqVO);
// 断言
assertEquals(1, pageResult.getTotal());
assertEquals(1, pageResult.getList().size());
assertPojoEquals(dbResult, pageResult.getList().get(0));
}
@Test
public void testGetIndicatorResultCountByQcIdAndType() {
// mock 数据
MesQcIndicatorResultDO result1 = randomPojo(MesQcIndicatorResultDO.class, o -> {
o.setQcId(1L);
o.setQcType(MesQcTypeEnum.IQC.getType());
});
resultMapper.insert(result1);
MesQcIndicatorResultDO result2 = randomPojo(MesQcIndicatorResultDO.class, o -> {
o.setQcId(1L);
o.setQcType(MesQcTypeEnum.IQC.getType());
});
resultMapper.insert(result2);
// 不匹配
resultMapper.insert(randomPojo(MesQcIndicatorResultDO.class, o -> {
o.setQcId(2L);
o.setQcType(MesQcTypeEnum.IPQC.getType());
}));
// 调用
Long count = indicatorResultService.getIndicatorResultCountByQcIdAndType(1L, MesQcTypeEnum.IQC.getType());
// 断言
assertEquals(2L, count);
}
@Test
public void testValidateIndicatorResultExistsByQcIdAndType_success() {
// mock 数据
resultMapper.insert(randomPojo(MesQcIndicatorResultDO.class, o -> {
o.setQcId(1L);
o.setQcType(MesQcTypeEnum.IQC.getType());
}));
// 调用(不抛异常即通过)
indicatorResultService.validateIndicatorResultExistsByQcIdAndType(1L, MesQcTypeEnum.IQC.getType());
}
@Test
public void testValidateIndicatorResultExistsByQcIdAndType_notExists() {
// 调用,并断言异常
assertServiceException(
() -> indicatorResultService.validateIndicatorResultExistsByQcIdAndType(randomLongId(), MesQcTypeEnum.IQC.getType()),
QC_FINISH_INDICATOR_RESULT_REQUIRED);
}
// ==================== 辅助方法 ====================
private void mockIqcAndIndicator(MesQcIndicatorResultSaveReqVO reqVO, Integer resultType) {
MesQcIqcDO iqc = new MesQcIqcDO();
iqc.setId(reqVO.getQcId());
iqc.setItemId(100L);
when(iqcService.validateIqcExists(reqVO.getQcId())).thenReturn(iqc);
MesQcIndicatorDO indicator = new MesQcIndicatorDO();
indicator.setId(reqVO.getItems().get(0).getIndicatorId());
indicator.setName("检测项A");
indicator.setResultType(resultType);
when(indicatorService.validateIndicatorListExists(anySet()))
.thenReturn(MapUtil.of(indicator.getId(), indicator));
}
private MesQcIndicatorResultSaveReqVO buildReqVO(String value, Integer resultType) {
MesQcIndicatorResultSaveReqVO.Item item = new MesQcIndicatorResultSaveReqVO.Item();
item.setIndicatorId(10L);
item.setValue(value);
MesQcIndicatorResultSaveReqVO reqVO = new MesQcIndicatorResultSaveReqVO();
reqVO.setCode("SPL-001");
reqVO.setQcId(1L);
reqVO.setQcType(MesQcTypeEnum.IQC.getType());
reqVO.setItems(ListUtil.of(item));
return reqVO;
}
}

View File

@@ -0,0 +1,596 @@
package cn.iocoder.yudao.module.mes.service.qc.ipqc;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.feedback.MesProFeedbackService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.math.BigDecimal;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* {@link MesQcIpqcServiceImpl} 的单元测试
*
* @author 芋道源码
*/
@Import(MesQcIpqcServiceImpl.class)
public class MesQcIpqcServiceImplTest extends BaseDbUnitTest {
@Resource
private MesQcIpqcServiceImpl ipqcService;
@Resource
private MesQcIpqcMapper ipqcMapper;
@MockitoBean
private MesQcTemplateItemService templateItemService;
@MockitoBean
private MesQcIpqcLineService ipqcLineService;
@MockitoBean
private MesProWorkOrderService workOrderService;
@MockitoBean
private MesMdWorkstationService workstationService;
@MockitoBean
private MesMdItemService itemService;
@MockitoBean
private MesQcDefectRecordService defectRecordService;
@MockitoBean
private AdminUserApi adminUserApi;
@MockitoBean
private MesProFeedbackService feedbackService;
@MockitoBean
private MesProRouteProductService routeProductService;
@MockitoBean
private MesProRouteProcessService routeProcessService;
@MockitoBean
private MesProTaskService taskService;
@MockitoBean
private MesQcIndicatorResultService indicatorResultService;
@MockitoBean
private MesWmProductProduceLineService productProduceLineService;
@Test
public void testFinishIpqc_writeBack_feedback() {
// 准备数据:插入一条草稿状态的 IPQC 单来源为报工单PRO_FEEDBACK=304
Long sourceDocId = randomLongId();
Long sourceLineId = randomLongId();
BigDecimal qualifiedQty = BigDecimal.valueOf(80);
BigDecimal unqualifiedQty = BigDecimal.valueOf(20);
BigDecimal laborScrapQty = BigDecimal.valueOf(5);
BigDecimal materialScrapQty = BigDecimal.valueOf(10);
BigDecimal otherScrapQty = BigDecimal.valueOf(5);
MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setCheckResult(1); // 非空,满足 finishIpqc 的校验
o.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
o.setSourceDocId(sourceDocId);
o.setSourceLineId(sourceLineId);
o.setQualifiedQuantity(qualifiedQty);
o.setUnqualifiedQuantity(unqualifiedQty);
o.setLaborScrapQuantity(laborScrapQty);
o.setMaterialScrapQuantity(materialScrapQty);
o.setOtherScrapQuantity(otherScrapQty);
});
ipqcMapper.insert(ipqc);
// 调用
ipqcService.finishIpqc(ipqc.getId());
// 断言 1状态更新为已完成
MesQcIpqcDO updatedIpqc = ipqcMapper.selectById(ipqc.getId());
assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIpqc.getStatus());
// 断言 2调用了 feedbackService传递 sourceLineId
// 注意:数量经过 DB 存取后 scale 可能变化(例如 80 → 80.00),所以用 any() 匹配
verify(feedbackService).updateProFeedbackWhenIpqcFinish(
eq(sourceDocId),
eq(sourceLineId),
any(BigDecimal.class),
any(BigDecimal.class),
any(BigDecimal.class),
any(BigDecimal.class),
any(BigDecimal.class));
}
@Test
public void testFinishIpqc_writeBack_feedback_withNullQuantities() {
// 准备数据:合格品/不合格品/废品数量为 null 的场景,应该 defaultIfNull 为 0
Long sourceDocId = randomLongId();
Long sourceLineId = randomLongId();
MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setCheckResult(1);
o.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
o.setSourceDocId(sourceDocId);
o.setSourceLineId(sourceLineId);
o.setQualifiedQuantity(null);
o.setUnqualifiedQuantity(null);
o.setLaborScrapQuantity(null);
o.setMaterialScrapQuantity(null);
o.setOtherScrapQuantity(null);
});
ipqcMapper.insert(ipqc);
// 调用
ipqcService.finishIpqc(ipqc.getId());
// 断言:所有数量参数都是 BigDecimal.ZEROdefaultIfNull 处理)
verify(feedbackService).updateProFeedbackWhenIpqcFinish(
eq(sourceDocId),
eq(sourceLineId),
eq(BigDecimal.ZERO),
eq(BigDecimal.ZERO),
eq(BigDecimal.ZERO),
eq(BigDecimal.ZERO),
eq(BigDecimal.ZERO));
}
@Test
public void testFinishIpqc_writeBack_noSourceDoc() {
// 准备数据sourceDocType 为空,不需要回写
MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setCheckResult(1);
o.setSourceDocType(null);
o.setSourceDocId(null);
o.setSourceLineId(null);
});
ipqcMapper.insert(ipqc);
// 调用
ipqcService.finishIpqc(ipqc.getId());
// 断言 1状态更新成功
MesQcIpqcDO updatedIpqc = ipqcMapper.selectById(ipqc.getId());
assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIpqc.getStatus());
// 断言 2不应该调用 feedbackService
verify(feedbackService, never()).updateProFeedbackWhenIpqcFinish(
anyLong(), anyLong(), any(), any(), any(), any(), any());
}
@Test
public void testFinishIpqc_writeBack_sourceDocTypeDirtyData() {
// 准备数据sourceDocType 非空但 sourceDocId 为空,应该抛异常
MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setCheckResult(1);
o.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
o.setSourceDocId(null); // 脏数据
o.setSourceLineId(null);
});
ipqcMapper.insert(ipqc);
// 调用,并断言异常
assertThrows(IllegalArgumentException.class, () -> ipqcService.finishIpqc(ipqc.getId()));
}
@Test
public void testFinishIpqc_writeBack_unknownSourceDocType() {
// 准备数据sourceDocType 无法识别
MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setCheckResult(1);
o.setSourceDocType(99999); // 无法识别的类型
o.setSourceDocId(randomLongId());
o.setSourceLineId(null);
});
ipqcMapper.insert(ipqc);
// 调用,并断言异常
assertThrows(IllegalArgumentException.class, () -> ipqcService.finishIpqc(ipqc.getId()));
}
@Test
public void testFinishIpqc_checkResultEmpty() {
// 准备数据checkResult 为空,应该报错
MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setCheckResult(null); // 检测结论为空
o.setSourceDocType(null);
});
ipqcMapper.insert(ipqc);
// 调用,应该抛异常(检测结论必填)
assertThrows(Exception.class, () -> ipqcService.finishIpqc(ipqc.getId()));
}
@Test
public void testFinishIpqc_notDraftStatus() {
// 准备数据:已完成状态,不能再次完成
MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.FINISHED.getStatus());
o.setCheckResult(1);
});
ipqcMapper.insert(ipqc);
// 调用,应该抛异常(不是草稿状态)
assertThrows(Exception.class, () -> ipqcService.finishIpqc(ipqc.getId()));
}
// ==================== processId 推导行为测试 ====================
/**
* 工位有工序 + 该工序在产品工艺路线中 → processId 应该被设置为工位工序
*/
@Test
public void testCreateIpqc_processId_workstationHasProcess() {
// 准备参数
Long workstationId = randomLongId();
Long workOrderId = randomLongId();
Long productId = randomLongId();
Long processId = randomLongId();
Long routeId = randomLongId();
Long templateId = randomLongId();
MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
reqVO.setCode("IPQC-TEST-001");
reqVO.setWorkstationId(workstationId);
reqVO.setWorkOrderId(workOrderId);
reqVO.setInspectorUserId(randomLongId());
// mock 工位返回有工序
MesMdWorkstationDO workstation = new MesMdWorkstationDO();
workstation.setId(workstationId);
workstation.setProcessId(processId);
when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
// mock 工单
MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
workOrder.setId(workOrderId);
workOrder.setProductId(productId);
when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
// mock 模板匹配
MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
templateItem.setTemplateId(templateId);
when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
// mock 工艺路线
MesProRouteProductDO routeProduct = new MesProRouteProductDO();
routeProduct.setRouteId(routeId);
when(routeProductService.getRouteProductByItemId(productId)).thenReturn(routeProduct);
// mock 工序在路线中
when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, processId))
.thenReturn(new MesProRouteProcessDO());
// 调用
Long ipqcId = ipqcService.createIpqc(reqVO);
// 断言processId 应该是工位工序
MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
assertEquals(processId, ipqc.getProcessId());
}
/**
* 工位没有工序processId=null→ processId 应该留 null不回退到任务工序
*/
@Test
public void testCreateIpqc_processId_workstationNoProcess_shouldBeNull() {
// 准备参数
Long workstationId = randomLongId();
Long workOrderId = randomLongId();
Long productId = randomLongId();
Long taskId = randomLongId();
Long templateId = randomLongId();
MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
reqVO.setCode("IPQC-TEST-002");
reqVO.setWorkstationId(workstationId);
reqVO.setWorkOrderId(workOrderId);
reqVO.setTaskId(taskId); // 故意传入 taskId验证不会回退
reqVO.setInspectorUserId(randomLongId());
// mock 工位没有工序
MesMdWorkstationDO workstation = new MesMdWorkstationDO();
workstation.setId(workstationId);
workstation.setProcessId(null); // 无工序
when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
// mock 工单
MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
workOrder.setId(workOrderId);
workOrder.setProductId(productId);
when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
// mock 任务
MesProTaskDO task = new MesProTaskDO();
task.setId(taskId);
task.setWorkOrderId(workOrderId);
task.setWorkstationId(workstationId);
task.setItemId(productId);
when(taskService.validateTaskNotFinished(taskId)).thenReturn(task);
// mock 模板匹配
MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
templateItem.setTemplateId(templateId);
when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
// 调用
Long ipqcId = ipqcService.createIpqc(reqVO);
// 断言processId 应该是 null即使传入了 taskId 也不应该回退
MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
assertNull(ipqc.getProcessId(),
"工位无工序时 processId 应为 null不应回退到任务工序");
// 断言:不应该读取任务的 processId
verify(routeProductService, never()).getRouteProductByItemId(any());
}
/**
* 工位有工序但该工序不在产品工艺路线中 → processId 应该留 null
*/
@Test
public void testCreateIpqc_processId_processNotInRoute() {
// 准备参数
Long workstationId = randomLongId();
Long workOrderId = randomLongId();
Long productId = randomLongId();
Long processId = randomLongId();
Long routeId = randomLongId();
Long templateId = randomLongId();
MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
reqVO.setCode("IPQC-TEST-003");
reqVO.setWorkstationId(workstationId);
reqVO.setWorkOrderId(workOrderId);
reqVO.setInspectorUserId(randomLongId());
// mock 工位有工序
MesMdWorkstationDO workstation = new MesMdWorkstationDO();
workstation.setId(workstationId);
workstation.setProcessId(processId);
when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
// mock 工单
MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
workOrder.setId(workOrderId);
workOrder.setProductId(productId);
when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
// mock 模板匹配
MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
templateItem.setTemplateId(templateId);
when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
// mock 工艺路线存在
MesProRouteProductDO routeProduct = new MesProRouteProductDO();
routeProduct.setRouteId(routeId);
when(routeProductService.getRouteProductByItemId(productId)).thenReturn(routeProduct);
// mock 工序不在路线中
when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, processId))
.thenReturn(null);
// 调用
Long ipqcId = ipqcService.createIpqc(reqVO);
// 断言processId 应该是 null
MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
assertNull(ipqc.getProcessId());
}
// ==================== sourceLineId 校验测试 ====================
/**
* sourceDocType=PRO_FEEDBACK 时 sourceLineId 为空 → 创建失败
*/
@Test
public void testCreateIpqc_sourceLineId_required() {
// 准备参数
Long workstationId = randomLongId();
Long workOrderId = randomLongId();
Long productId = randomLongId();
Long feedbackId = randomLongId();
Long templateId = randomLongId();
MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
reqVO.setCode("IPQC-SL-001");
reqVO.setWorkstationId(workstationId);
reqVO.setWorkOrderId(workOrderId);
reqVO.setInspectorUserId(randomLongId());
reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
reqVO.setSourceDocId(feedbackId);
reqVO.setSourceLineId(null); // 不传 sourceLineId
// mock
MesMdWorkstationDO workstation = new MesMdWorkstationDO();
workstation.setId(workstationId);
workstation.setProcessId(null);
when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
workOrder.setId(workOrderId);
workOrder.setProductId(productId);
when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
templateItem.setTemplateId(templateId);
when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
// 调用,断言异常
ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
assertEquals(QC_IPQC_SOURCE_LINE_REQUIRED.getCode(), ex.getCode());
}
/**
* sourceLineId 不属于该报工的产出行 → 创建失败
*/
@Test
public void testCreateIpqc_sourceLineId_notBelong() {
// 准备参数
Long workstationId = randomLongId();
Long workOrderId = randomLongId();
Long productId = randomLongId();
Long feedbackId = randomLongId();
Long sourceLineId = randomLongId();
Long templateId = randomLongId();
MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
reqVO.setCode("IPQC-SL-002");
reqVO.setWorkstationId(workstationId);
reqVO.setWorkOrderId(workOrderId);
reqVO.setInspectorUserId(randomLongId());
reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
reqVO.setSourceDocId(feedbackId);
reqVO.setSourceLineId(sourceLineId);
// mock
MesMdWorkstationDO workstation = new MesMdWorkstationDO();
workstation.setId(workstationId);
workstation.setProcessId(null);
when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
workOrder.setId(workOrderId);
workOrder.setProductId(productId);
when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
templateItem.setTemplateId(templateId);
when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
// mock产出行存在但 feedbackId 不匹配
MesWmProductProduceLineDO line = new MesWmProductProduceLineDO();
line.setId(sourceLineId);
line.setFeedbackId(randomLongId()); // 不同的 feedbackId
line.setQualityStatus(MesWmQualityStatusEnum.PENDING.getStatus());
when(productProduceLineService.validateProductProduceLineExists(sourceLineId)).thenReturn(line);
// 调用,断言异常
ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
assertEquals(QC_IPQC_SOURCE_LINE_NOT_BELONG.getCode(), ex.getCode());
}
/**
* sourceLineId 对应的产出行不是待检验状态 → 创建失败
*/
@Test
public void testCreateIpqc_sourceLineId_notPending() {
// 准备参数
Long workstationId = randomLongId();
Long workOrderId = randomLongId();
Long productId = randomLongId();
Long feedbackId = randomLongId();
Long sourceLineId = randomLongId();
Long templateId = randomLongId();
MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
reqVO.setCode("IPQC-SL-003");
reqVO.setWorkstationId(workstationId);
reqVO.setWorkOrderId(workOrderId);
reqVO.setInspectorUserId(randomLongId());
reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
reqVO.setSourceDocId(feedbackId);
reqVO.setSourceLineId(sourceLineId);
// mock
MesMdWorkstationDO workstation = new MesMdWorkstationDO();
workstation.setId(workstationId);
workstation.setProcessId(null);
when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
workOrder.setId(workOrderId);
workOrder.setProductId(productId);
when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
templateItem.setTemplateId(templateId);
when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(new MesProFeedbackDO());
// mock产出行存在且归属正确但不是 PENDING 状态
MesWmProductProduceLineDO line = new MesWmProductProduceLineDO();
line.setId(sourceLineId);
line.setFeedbackId(feedbackId);
line.setQualityStatus(MesWmQualityStatusEnum.PASS.getStatus()); // 已合格,非待检
when(productProduceLineService.validateProductProduceLineExists(sourceLineId)).thenReturn(line);
// 调用,断言异常
ServiceException ex = assertThrows(ServiceException.class, () -> ipqcService.createIpqc(reqVO));
assertEquals(QC_IPQC_SOURCE_LINE_NOT_PENDING.getCode(), ex.getCode());
}
/**
* sourceLineId 校验全部通过 → 创建成功
*/
@Test
public void testCreateIpqc_sourceLineId_success() {
// 准备参数
Long workstationId = randomLongId();
Long workOrderId = randomLongId();
Long productId = randomLongId();
Long feedbackId = randomLongId();
Long sourceLineId = randomLongId();
Long templateId = randomLongId();
MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
reqVO.setCode("IPQC-SL-004");
reqVO.setWorkstationId(workstationId);
reqVO.setWorkOrderId(workOrderId);
reqVO.setInspectorUserId(randomLongId());
reqVO.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
reqVO.setSourceDocId(feedbackId);
reqVO.setSourceLineId(sourceLineId);
// mock
MesMdWorkstationDO workstation = new MesMdWorkstationDO();
workstation.setId(workstationId);
workstation.setProcessId(null);
when(workstationService.validateWorkstationExists(workstationId)).thenReturn(workstation);
MesProWorkOrderDO workOrder = new MesProWorkOrderDO();
workOrder.setId(workOrderId);
workOrder.setProductId(productId);
when(workOrderService.validateWorkOrderExists(workOrderId)).thenReturn(workOrder);
when(workOrderService.validateWorkOrderConfirmed(workOrderId)).thenReturn(workOrder);
MesQcTemplateItemDO templateItem = new MesQcTemplateItemDO();
templateItem.setTemplateId(templateId);
when(templateItemService.getRequiredTemplateByItemIdAndType(eq(productId), anyInt())).thenReturn(templateItem);
MesProFeedbackDO feedback = new MesProFeedbackDO();
feedback.setId(feedbackId);
feedback.setCode("FB-001");
when(feedbackService.validateFeedbackExists(feedbackId)).thenReturn(feedback);
// mock产出行存在、归属正确、PENDING 状态
MesWmProductProduceLineDO line = new MesWmProductProduceLineDO();
line.setId(sourceLineId);
line.setFeedbackId(feedbackId);
line.setQualityStatus(MesWmQualityStatusEnum.PENDING.getStatus());
when(productProduceLineService.validateProductProduceLineExists(sourceLineId)).thenReturn(line);
// 调用
Long ipqcId = ipqcService.createIpqc(reqVO);
// 断言创建成功sourceDocCode 回写
MesQcIpqcDO ipqc = ipqcMapper.selectById(ipqcId);
assertNotNull(ipqc);
assertEquals(feedbackId, ipqc.getSourceDocId());
assertEquals(sourceLineId, ipqc.getSourceLineId());
assertEquals("FB-001", ipqc.getSourceDocCode());
}
}

View File

@@ -0,0 +1,206 @@
package cn.iocoder.yudao.module.mes.service.qc.iqc;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.iqc.MesQcIqcDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.iqc.MesQcIqcMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.vendor.MesMdVendorService;
import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeService;
import cn.iocoder.yudao.module.mes.service.wm.outsourcereceipt.MesWmOutsourceReceiptService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import jakarta.annotation.Resource;
import java.math.BigDecimal;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* {@link MesQcIqcServiceImpl} 的单元测试
*
* @author 芋道源码
*/
@Import(MesQcIqcServiceImpl.class)
public class MesQcIqcServiceImplTest extends BaseDbUnitTest {
@Resource
private MesQcIqcServiceImpl iqcService;
@Resource
private MesQcIqcMapper iqcMapper;
@MockitoBean
private MesWmArrivalNoticeService arrivalNoticeService;
@MockitoBean
private MesWmOutsourceReceiptService outsourceReceiptService;
@MockitoBean
private MesQcIqcLineService iqcLineService;
@MockitoBean
private MesQcDefectRecordService defectRecordService;
@MockitoBean
private MesMdVendorService vendorService;
@MockitoBean
private MesMdItemService itemService;
@MockitoBean
private MesQcTemplateItemService templateItemService;
@MockitoBean
private AdminUserApi adminUserApi;
@MockitoBean
private MesQcIndicatorResultService indicatorResultService;
@Test
public void testFinishIqc_writeBack_arrivalNotice() {
// 准备数据:插入一条草稿状态的 IQC 单,来源为到货通知单
Long sourceDocId = randomLongId();
Long sourceLineId = randomLongId();
MesQcIqcDO iqc = randomPojo(MesQcIqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setCheckResult(1); // 非空,满足 finishIqc 的校验
o.setSourceDocType(MesBizTypeConstants.WM_ARRIVAL_NOTICE);
o.setSourceDocId(sourceDocId);
o.setSourceLineId(sourceLineId);
o.setQualifiedQuantity(BigDecimal.valueOf(100));
o.setUnqualifiedQuantity(BigDecimal.ZERO);
});
iqcMapper.insert(iqc);
// 调用
iqcService.finishIqc(iqc.getId());
// 断言:验证状态更新为已完成
MesQcIqcDO updatedIqc = iqcMapper.selectById(iqc.getId());
assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIqc.getStatus());
// 断言:验证回写到货通知单(参数使用 any 避免 BigDecimal 精度问题)
verify(arrivalNoticeService).updateArrivalNoticeWhenIqcFinish(
eq(sourceDocId), eq(sourceLineId), eq(iqc.getId()), any(BigDecimal.class));
// 断言:不应该调用外协入库单的回写
verify(outsourceReceiptService, never()).updateOutsourceReceiptWhenIqcFinish(
anyLong(), anyLong(), anyLong(), any(), any());
}
@Test
public void testFinishIqc_writeBack_outsourceReceipt() {
// 准备数据:插入一条草稿状态的 IQC 单,来源为外协入库单
Long sourceDocId = randomLongId();
Long sourceLineId = randomLongId();
MesQcIqcDO iqc = randomPojo(MesQcIqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setCheckResult(1);
o.setSourceDocType(MesBizTypeConstants.WM_OUTSOURCE_RECPT);
o.setSourceDocId(sourceDocId);
o.setSourceLineId(sourceLineId);
o.setQualifiedQuantity(BigDecimal.valueOf(80));
o.setUnqualifiedQuantity(BigDecimal.valueOf(20));
});
iqcMapper.insert(iqc);
// 调用
iqcService.finishIqc(iqc.getId());
// 断言:验证状态更新为已完成
MesQcIqcDO updatedIqc = iqcMapper.selectById(iqc.getId());
assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIqc.getStatus());
// 断言:验证回写外协入库单(参数使用 any 避免 BigDecimal 精度问题)
verify(outsourceReceiptService).updateOutsourceReceiptWhenIqcFinish(
eq(sourceDocId), eq(sourceLineId), eq(iqc.getId()), any(BigDecimal.class), any(BigDecimal.class));
// 断言:不应该调用到货通知单的回写
verify(arrivalNoticeService, never()).updateArrivalNoticeWhenIqcFinish(
anyLong(), anyLong(), anyLong(), any());
}
@Test
public void testFinishIqc_writeBack_noSourceDoc() {
// 准备数据sourceDocType 为空,不需要回写
MesQcIqcDO iqc = randomPojo(MesQcIqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setCheckResult(1);
o.setSourceDocType(null);
o.setSourceDocId(null);
o.setSourceLineId(null);
});
iqcMapper.insert(iqc);
// 调用
iqcService.finishIqc(iqc.getId());
// 断言:验证状态更新为已完成
MesQcIqcDO updatedIqc = iqcMapper.selectById(iqc.getId());
assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIqc.getStatus());
// 断言:不应该调用任何回写
verify(arrivalNoticeService, never()).updateArrivalNoticeWhenIqcFinish(
anyLong(), anyLong(), anyLong(), any());
verify(outsourceReceiptService, never()).updateOutsourceReceiptWhenIqcFinish(
anyLong(), anyLong(), anyLong(), any(), any());
}
@Test
public void testFinishIqc_writeBack_sourceDocTypeDirtyData() {
// 准备数据sourceDocType 非空但 sourceLineId 为空,应该抛异常
MesQcIqcDO iqc = randomPojo(MesQcIqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setCheckResult(1);
o.setSourceDocType(MesBizTypeConstants.WM_ARRIVAL_NOTICE);
o.setSourceDocId(randomLongId());
o.setSourceLineId(null); // 脏数据
});
iqcMapper.insert(iqc);
// 调用,并断言异常
assertThrows(IllegalArgumentException.class, () -> iqcService.finishIqc(iqc.getId()));
}
@Test
public void testFinishIqc_writeBack_unknownSourceDocType() {
// 准备数据sourceDocType 无法识别
MesQcIqcDO iqc = randomPojo(MesQcIqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setCheckResult(1);
o.setSourceDocType(99999); // 无法识别的类型
o.setSourceDocId(randomLongId());
o.setSourceLineId(randomLongId());
});
iqcMapper.insert(iqc);
// 调用,并断言异常
assertThrows(IllegalArgumentException.class, () -> iqcService.finishIqc(iqc.getId()));
}
@Test
public void testFinishIqc_checkResultEmpty() {
// 准备数据checkResult 为空,应该报错
MesQcIqcDO iqc = randomPojo(MesQcIqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setCheckResult(null); // 检测结论为空
o.setSourceDocType(null);
});
iqcMapper.insert(iqc);
// 调用,应该抛异常(检测结论必填)
assertThrows(Exception.class, () -> iqcService.finishIqc(iqc.getId()));
}
@Test
public void testFinishIqc_notDraftStatus() {
// 准备数据:已完成状态,不能再次完成
MesQcIqcDO iqc = randomPojo(MesQcIqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.FINISHED.getStatus());
o.setCheckResult(1);
});
iqcMapper.insert(iqc);
// 调用,应该抛异常(不是草稿状态)
assertThrows(Exception.class, () -> iqcService.finishIqc(iqc.getId()));
}
}

View File

@@ -0,0 +1,203 @@
package cn.iocoder.yudao.module.mes.service.qc.oqc;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.client.MesMdClientService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesLineService;
import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
/**
* {@link MesQcOqcServiceImpl} 的单元测试类
*
* @author 芋道源码
*/
@Import(MesQcOqcServiceImpl.class)
public class MesQcOqcServiceImplTest extends BaseDbUnitTest {
@Resource
private MesQcOqcServiceImpl oqcService;
@Resource
private MesQcOqcMapper oqcMapper;
@MockitoBean
private MesWmProductSalesLineService productSalesLineService;
@MockitoBean
private MesWmProductSalesService productSalesService;
@MockitoBean
private MesQcOqcLineService oqcLineService;
@MockitoBean
private MesMdClientService clientService;
@MockitoBean
private MesMdItemService itemService;
@MockitoBean
private MesQcTemplateItemService templateItemService;
@MockitoBean
private MesQcDefectRecordService defectRecordService;
@MockitoBean
private AdminUserApi adminUserApi;
@MockitoBean
private MesQcIndicatorResultService indicatorResultService;
@Test
public void testFinishOqc_writeBack_productSales() {
// 准备数据:插入一条草稿状态的 OQC 单,来源为销售出库单
Long sourceLineId = randomLongId();
Integer checkResult = 1;
MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES);
o.setSourceLineId(sourceLineId);
o.setCheckResult(checkResult);
});
oqcMapper.insert(oqc);
// 调用
oqcService.finishOqc(oqc.getId());
// 断言:验证状态更新为已完成
MesQcOqcDO updatedOqc = oqcMapper.selectById(oqc.getId());
assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedOqc.getStatus());
// 断言:验证回写销售出库单行
verify(productSalesLineService).updateProductSalesLineWhenOqcFinish(
eq(sourceLineId), eq(oqc.getId()), eq(checkResult));
}
@Test
public void testFinishOqc_writeBack_noSourceDoc() {
// 准备数据sourceDocType 为空,不需要回写
MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setCheckResult(1);
o.setSourceDocType(null);
o.setSourceLineId(null);
});
oqcMapper.insert(oqc);
// 调用
oqcService.finishOqc(oqc.getId());
// 断言:验证状态更新为已完成
MesQcOqcDO updatedOqc = oqcMapper.selectById(oqc.getId());
assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedOqc.getStatus());
// 断言:不应该调用任何回写
verify(productSalesLineService, never()).updateProductSalesLineWhenOqcFinish(
anyLong(), anyLong(), anyInt());
}
@Test
public void testFinishOqc_writeBack_sourceDocTypeDirtyData() {
// 准备数据sourceDocType 非空但 sourceLineId 为空,应该抛异常
MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setCheckResult(1);
o.setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES);
o.setSourceLineId(null); // 脏数据
});
oqcMapper.insert(oqc);
// 调用,并断言异常
assertThrows(IllegalArgumentException.class, () -> oqcService.finishOqc(oqc.getId()));
}
@Test
public void testFinishOqc_writeBack_unknownSourceDocType() {
// 准备数据sourceDocType 无法识别
MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setCheckResult(1);
o.setSourceDocType(99999); // 无法识别的类型
o.setSourceLineId(randomLongId());
});
oqcMapper.insert(oqc);
// 调用,并断言异常
assertThrows(IllegalArgumentException.class, () -> oqcService.finishOqc(oqc.getId()));
}
@Test
public void testFinishOqc_notExists() {
// 准备参数
Long oqcId = randomLongId();
// 调用,并断言异常
assertServiceException(() -> oqcService.finishOqc(oqcId), QC_OQC_NOT_EXISTS);
}
@Test
public void testFinishOqc_statusNotDraft() {
// 准备参数:状态为已完成
MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.FINISHED.getStatus());
});
oqcMapper.insert(oqc);
// 调用,并断言异常
assertServiceException(() -> oqcService.finishOqc(oqc.getId()), QC_OQC_NOT_PREPARE);
}
@Test
public void testFinishOqc_checkResultEmpty() {
// 准备数据checkResult 为空,应该报错
MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setCheckResult(null); // 检测结论为空
o.setSourceDocType(null);
});
oqcMapper.insert(oqc);
// 调用,应该抛异常(检测结论必填)
assertServiceException(() -> oqcService.finishOqc(oqc.getId()), QC_OQC_CHECK_RESULT_EMPTY);
}
@Test
public void testDeleteOqc_success() {
// 准备数据:草稿状态
MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
});
oqcMapper.insert(oqc);
// 调用
oqcService.deleteOqc(oqc.getId());
// 断言:已删除
assertNull(oqcMapper.selectById(oqc.getId()));
}
@Test
public void testDeleteOqc_notExists() {
assertServiceException(() -> oqcService.deleteOqc(randomLongId()), QC_OQC_NOT_EXISTS);
}
@Test
public void testDeleteOqc_statusNotDraft() {
MesQcOqcDO oqc = randomPojo(MesQcOqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.FINISHED.getStatus());
});
oqcMapper.insert(oqc);
assertServiceException(() -> oqcService.deleteOqc(oqc.getId()), QC_OQC_NOT_PREPARE);
}
}

View File

@@ -0,0 +1,165 @@
package cn.iocoder.yudao.module.mes.service.qc.rqc;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.rqc.MesQcRqcDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.rqc.MesQcRqcMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
import cn.iocoder.yudao.module.mes.service.wm.returnissue.MesWmReturnIssueLineService;
import cn.iocoder.yudao.module.mes.service.wm.returnsales.MesWmReturnSalesLineService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.math.BigDecimal;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.QC_RQC_NOT_EXISTS;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.QC_RQC_NOT_PREPARE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* {@link MesQcRqcServiceImpl} 的单元测试类
*
* @author 芋道源码
*/
@Import(MesQcRqcServiceImpl.class)
public class MesQcRqcServiceImplTest extends BaseDbUnitTest {
@Resource
private MesQcRqcServiceImpl rqcService;
@Resource
private MesQcRqcMapper rqcMapper;
@MockitoBean
private MesWmReturnIssueLineService returnIssueLineService;
@MockitoBean
private MesWmReturnSalesLineService returnSalesLineService;
@MockitoBean
private MesQcRqcLineService rqcLineService;
@MockitoBean
private MesMdItemService itemService;
@MockitoBean
private MesQcTemplateItemService templateItemService;
@MockitoBean
private MesQcDefectRecordService defectRecordService;
@MockitoBean
private AdminUserApi adminUserApi;
@MockitoBean
private MesQcIndicatorResultService indicatorResultService;
@Test
public void testFinishRqc_successWithReturnIssue() {
// 准备参数
Long sourceLineId = randomLongId();
Long sourceDocId = randomLongId();
Integer checkResult = 1;
BigDecimal qualifiedQty = new BigDecimal("80");
BigDecimal unqualifiedQty = new BigDecimal("20");
// mock 数据
MesQcRqcDO rqc = randomPojo(MesQcRqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setSourceDocType(MesBizTypeConstants.WM_RETURN_ISSUE);
o.setSourceDocId(sourceDocId);
o.setSourceLineId(sourceLineId);
o.setCheckResult(checkResult);
// 设置数量通过 finishRqc 的数量校验:合格 + 不合格 = 检测数量
o.setCheckQuantity(new BigDecimal("100"));
o.setQualifiedQuantity(qualifiedQty);
o.setUnqualifiedQuantity(unqualifiedQty);
});
rqcMapper.insert(rqc);
// 调用
rqcService.finishRqc(rqc.getId());
// 断言:验证状态更新
MesQcRqcDO updatedRqc = rqcMapper.selectById(rqc.getId());
assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedRqc.getStatus());
// 断言:验证回写调用生产退料(含拆分参数)
verify(returnIssueLineService).updateReturnIssueLineWhenRqcFinish(
eq(sourceLineId), eq(sourceDocId), eq(checkResult),
argThat(v -> v.compareTo(qualifiedQty) == 0),
argThat(v -> v.compareTo(unqualifiedQty) == 0));
// 断言:未调用销售退货
verify(returnSalesLineService, never()).updateReturnSalesLineWhenRqcFinish(
anyLong(), anyLong(), anyInt(), any(BigDecimal.class), any(BigDecimal.class));
}
@Test
public void testFinishRqc_successWithReturnSales() {
// 准备参数
Long sourceLineId = randomLongId();
Long sourceDocId = randomLongId();
Integer checkResult = 1;
BigDecimal qualifiedQty = new BigDecimal("90");
BigDecimal unqualifiedQty = new BigDecimal("10");
// mock 数据
MesQcRqcDO rqc = randomPojo(MesQcRqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
o.setSourceDocType(MesBizTypeConstants.WM_RETURN_SALES);
o.setSourceDocId(sourceDocId);
o.setSourceLineId(sourceLineId);
o.setCheckResult(checkResult);
// 设置数量通过 finishRqc 的数量校验:合格 + 不合格 = 检测数量
o.setCheckQuantity(new BigDecimal("100"));
o.setQualifiedQuantity(qualifiedQty);
o.setUnqualifiedQuantity(unqualifiedQty);
});
rqcMapper.insert(rqc);
// 调用
rqcService.finishRqc(rqc.getId());
// 断言:验证状态更新
MesQcRqcDO updatedRqc = rqcMapper.selectById(rqc.getId());
assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedRqc.getStatus());
// 断言:验证回写调用销售退货(含拆分参数)
verify(returnSalesLineService).updateReturnSalesLineWhenRqcFinish(
eq(sourceLineId), eq(sourceDocId), eq(checkResult),
argThat(v -> v.compareTo(qualifiedQty) == 0),
argThat(v -> v.compareTo(unqualifiedQty) == 0));
// 断言:未调用生产退料
verify(returnIssueLineService, never()).updateReturnIssueLineWhenRqcFinish(
anyLong(), anyLong(), anyInt(), any(BigDecimal.class), any(BigDecimal.class));
}
@Test
public void testFinishRqc_notExists() {
// 准备参数
Long rqcId = randomLongId();
// 调用,并断言异常
assertServiceException(() -> rqcService.finishRqc(rqcId), QC_RQC_NOT_EXISTS);
}
@Test
public void testFinishRqc_statusNotDraft() {
// 准备参数
// mock 数据:状态为已完成
MesQcRqcDO rqc = randomPojo(MesQcRqcDO.class, o -> {
o.setStatus(MesQcStatusEnum.FINISHED.getStatus());
});
rqcMapper.insert(rqc);
// 调用,并断言异常
assertServiceException(() -> rqcService.finishRqc(rqc.getId()), QC_RQC_NOT_PREPARE);
}
}

View File

@@ -0,0 +1,152 @@
package cn.iocoder.yudao.module.mes.service.wm.arrivalnotice;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.arrivalnotice.MesWmArrivalNoticeMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmArrivalNoticeStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.vendor.MesMdVendorService;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import jakarta.annotation.Resource;
import java.math.BigDecimal;
import java.util.Arrays;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
/**
* {@link MesWmArrivalNoticeServiceImpl} 的单元测试
*
* @author 芋道源码
*/
@Import(MesWmArrivalNoticeServiceImpl.class)
public class MesWmArrivalNoticeServiceImplTest extends BaseDbUnitTest {
@Resource
private MesWmArrivalNoticeServiceImpl arrivalNoticeService;
@Resource
private MesWmArrivalNoticeMapper arrivalNoticeMapper;
@MockitoBean
private MesWmArrivalNoticeLineService arrivalNoticeLineService;
@MockitoBean
private MesMdVendorService vendorService;
@Test
public void testUpdateArrivalNoticeWhenIqcFinish_success_allLinesChecked() {
// mock 数据:插入一条待质检的到货通知单
MesWmArrivalNoticeDO notice = randomPojo(MesWmArrivalNoticeDO.class, o -> {
o.setStatus(MesWmArrivalNoticeStatusEnum.PENDING_QC.getStatus());
});
arrivalNoticeMapper.insert(notice);
// 准备参数
Long noticeId = notice.getId();
Long lineId = randomLongId();
Long iqcId = randomLongId();
BigDecimal qualifiedQuantity = BigDecimal.valueOf(100);
// mock 行服务方法所有需检行都已完成iqcId 不为空)
MesWmArrivalNoticeLineDO line1 = randomPojo(MesWmArrivalNoticeLineDO.class, o -> {
o.setNoticeId(noticeId);
o.setIqcCheckFlag(true);
o.setIqcId(iqcId); // 已检完
});
MesWmArrivalNoticeLineDO line2 = randomPojo(MesWmArrivalNoticeLineDO.class, o -> {
o.setNoticeId(noticeId);
o.setIqcCheckFlag(false); // 不需要检验
o.setIqcId(null);
});
when(arrivalNoticeLineService.getArrivalNoticeLineListByNoticeId(eq(noticeId)))
.thenReturn(Arrays.asList(line1, line2));
// 调用
arrivalNoticeService.updateArrivalNoticeWhenIqcFinish(noticeId, lineId, iqcId, qualifiedQuantity);
// 断言:行回写被调用
verify(arrivalNoticeLineService).updateArrivalNoticeLineWhenIqcFinish(eq(lineId), eq(iqcId), eq(qualifiedQuantity));
// 断言:主表状态更新为待入库
MesWmArrivalNoticeDO updatedNotice = arrivalNoticeMapper.selectById(noticeId);
assertEquals(MesWmArrivalNoticeStatusEnum.PENDING_RECEIPT.getStatus(), updatedNotice.getStatus());
}
@Test
public void testUpdateArrivalNoticeWhenIqcFinish_success_hasUncheckedLines() {
// mock 数据:插入一条待质检的到货通知单
MesWmArrivalNoticeDO notice = randomPojo(MesWmArrivalNoticeDO.class, o -> {
o.setStatus(MesWmArrivalNoticeStatusEnum.PENDING_QC.getStatus());
});
arrivalNoticeMapper.insert(notice);
// 准备参数
Long noticeId = notice.getId();
Long lineId = randomLongId();
Long iqcId = randomLongId();
BigDecimal qualifiedQuantity = BigDecimal.valueOf(50);
// mock 行服务方法:还有未完成的需检行
MesWmArrivalNoticeLineDO line1 = randomPojo(MesWmArrivalNoticeLineDO.class, o -> {
o.setNoticeId(noticeId);
o.setIqcCheckFlag(true);
o.setIqcId(iqcId); // 已检完
});
MesWmArrivalNoticeLineDO line2 = randomPojo(MesWmArrivalNoticeLineDO.class, o -> {
o.setNoticeId(noticeId);
o.setIqcCheckFlag(true);
o.setIqcId(null); // 还未检验
});
when(arrivalNoticeLineService.getArrivalNoticeLineListByNoticeId(eq(noticeId)))
.thenReturn(Arrays.asList(line1, line2));
// 调用
arrivalNoticeService.updateArrivalNoticeWhenIqcFinish(noticeId, lineId, iqcId, qualifiedQuantity);
// 断言:行回写被调用
verify(arrivalNoticeLineService).updateArrivalNoticeLineWhenIqcFinish(eq(lineId), eq(iqcId), eq(qualifiedQuantity));
// 断言:主表状态不变,仍然是待质检
MesWmArrivalNoticeDO updatedNotice = arrivalNoticeMapper.selectById(noticeId);
assertEquals(MesWmArrivalNoticeStatusEnum.PENDING_QC.getStatus(), updatedNotice.getStatus());
}
@Test
public void testUpdateArrivalNoticeWhenIqcFinish_noticeNotExists() {
// 准备参数
Long noticeId = randomLongId();
Long lineId = randomLongId();
Long iqcId = randomLongId();
BigDecimal qualifiedQuantity = BigDecimal.valueOf(100);
// 调用,并断言异常
assertServiceException(
() -> arrivalNoticeService.updateArrivalNoticeWhenIqcFinish(noticeId, lineId, iqcId, qualifiedQuantity),
WM_ARRIVAL_NOTICE_NOT_EXISTS);
}
@Test
public void testUpdateArrivalNoticeWhenIqcFinish_statusNotPendingQc() {
// mock 数据:插入一条草稿状态的到货通知单(非待质检)
MesWmArrivalNoticeDO notice = randomPojo(MesWmArrivalNoticeDO.class, o -> {
o.setStatus(MesWmArrivalNoticeStatusEnum.PREPARE.getStatus());
});
arrivalNoticeMapper.insert(notice);
// 准备参数
Long noticeId = notice.getId();
Long lineId = randomLongId();
Long iqcId = randomLongId();
BigDecimal qualifiedQuantity = BigDecimal.valueOf(100);
// 调用,并断言异常
assertServiceException(
() -> arrivalNoticeService.updateArrivalNoticeWhenIqcFinish(noticeId, lineId, iqcId, qualifiedQuantity),
WM_ARRIVAL_NOTICE_STATUS_NOT_PENDING_QC);
}
}

View File

@@ -0,0 +1,407 @@
package cn.iocoder.yudao.module.mes.service.wm.batch;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemconsume.MesWmItemConsumeDetailMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemconsume.MesWmItemConsumeMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productproduce.MesWmProductProduceDetailMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productproduce.MesWmProductProduceLineMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productproduce.MesWmProductProduceMapper;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemBatchConfigService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.wm.barcode.MesWmBarcodeService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link MesWmBatchServiceImpl} 的单元测试
*
* @author 芋道源码
*/
@Import(MesWmBatchServiceImpl.class)
public class MesWmBatchServiceImplTest extends BaseDbUnitTest {
@Resource
private MesWmBatchServiceImpl batchService;
@Resource
private MesWmBatchMapper batchMapper;
@Resource
private MesWmItemConsumeMapper consumeMapper;
@Resource
private MesWmItemConsumeDetailMapper consumeDetailMapper;
@Resource
private MesProFeedbackMapper feedbackMapper;
@Resource
private MesWmProductProduceMapper produceMapper;
@Resource
private MesWmProductProduceLineMapper produceLineMapper;
@Resource
private MesWmProductProduceDetailMapper produceDetailMapper;
@MockitoBean
private MesMdItemService itemService;
@MockitoBean
private MesMdItemBatchConfigService itemBatchConfigService;
@MockitoBean
private MesMdAutoCodeRecordService autoCodeRecordService;
@MockitoBean
private MesWmBarcodeService barcodeService;
// ==================== 向前追溯 ====================
@Test
public void testGetForwardBatchList_nullCode() {
// 传入 null code应返回空列表不抛异常
List<MesWmBatchDO> result = batchService.getForwardBatchList(null);
assertNotNull(result);
assertTrue(result.isEmpty());
}
@Test
public void testGetForwardBatchList_noResults() {
// 传入一个不存在的批次号XML 查询返回空(因为关联表无数据)
List<MesWmBatchDO> result = batchService.getForwardBatchList("NOT_EXIST_CODE");
assertNotNull(result);
assertTrue(result.isEmpty());
}
// ==================== 向后追溯 ====================
@Test
public void testGetBackwardBatchList_nullCode() {
// 传入 null code应返回空列表不抛异常
List<MesWmBatchDO> result = batchService.getBackwardBatchList(null);
assertNotNull(result);
assertTrue(result.isEmpty());
}
@Test
public void testGetBackwardBatchList_noResults() {
// 传入一个不存在的批次号XML 查询返回空
List<MesWmBatchDO> result = batchService.getBackwardBatchList("NOT_EXIST_CODE");
assertNotNull(result);
assertTrue(result.isEmpty());
}
// ==================== 循环检测 ====================
@Test
public void testForwardBatchList_sameCodeDoesNotRecurse() {
// 验证:传入相同 code 多次调用不会 StackOverflow
// 由于数据库中没有环路数据,这里主要验证 visited set 的 null + 空数据安全
List<MesWmBatchDO> result = batchService.getForwardBatchList("SAME_CODE");
assertNotNull(result);
assertTrue(result.isEmpty());
// 二次调用同一个 code不应有任何异常
List<MesWmBatchDO> result2 = batchService.getForwardBatchList("SAME_CODE");
assertNotNull(result2);
assertTrue(result2.isEmpty());
}
@Test
public void testBackwardBatchList_sameCodeDoesNotRecurse() {
// 验证向后追溯的 visited set 安全
List<MesWmBatchDO> result = batchService.getBackwardBatchList("SAME_CODE");
assertNotNull(result);
assertTrue(result.isEmpty());
}
// ==================== selectFirst ====================
@Test
public void testSelectFirst_noMatch() {
// 查询不存在的 itemId应返回 null
MesWmBatchDO query = MesWmBatchDO.builder().itemId(randomLongId()).build();
MesWmBatchDO result = batchMapper.selectFirst(query);
assertNull(result);
}
@Test
public void testSelectFirst_returnsSmallestId() {
// 准备数据:插入两条相同条件的批次
Long itemId = randomLongId();
MesWmBatchDO batch1 = MesWmBatchDO.builder()
.itemId(itemId).code("BATCH_001").build();
MesWmBatchDO batch2 = MesWmBatchDO.builder()
.itemId(itemId).code("BATCH_002").build();
batchMapper.insert(batch1);
batchMapper.insert(batch2);
// 查询
MesWmBatchDO query = MesWmBatchDO.builder().itemId(itemId).build();
MesWmBatchDO result = batchMapper.selectFirst(query);
// 断言:返回 ID 最小的
assertNotNull(result);
assertEquals(batch1.getId(), result.getId());
assertEquals("BATCH_001", result.getCode());
}
@Test
public void testSelectFirst_nullFieldMatching() {
// 准备数据:一条有 vendorId一条没有 vendorId
Long itemId = randomLongId();
Long vendorId = randomLongId();
MesWmBatchDO batchWithVendor = MesWmBatchDO.builder()
.itemId(itemId).code("BATCH_WITH_V").vendorId(vendorId).build();
MesWmBatchDO batchWithoutVendor = MesWmBatchDO.builder()
.itemId(itemId).code("BATCH_NO_V").build();
batchMapper.insert(batchWithVendor);
batchMapper.insert(batchWithoutVendor);
// 查询vendorId 为 null -> 应该只匹配 vendorId IS NULL 的记录
MesWmBatchDO query = MesWmBatchDO.builder().itemId(itemId).build();
MesWmBatchDO result = batchMapper.selectFirst(query);
assertNotNull(result);
assertEquals("BATCH_NO_V", result.getCode());
assertNull(result.getVendorId());
}
@Test
public void testSelectFirst_withVendorId() {
// 准备数据
Long itemId = randomLongId();
Long vendorId = randomLongId();
MesWmBatchDO batchWithVendor = MesWmBatchDO.builder()
.itemId(itemId).code("BATCH_V1").vendorId(vendorId).build();
MesWmBatchDO batchWithoutVendor = MesWmBatchDO.builder()
.itemId(itemId).code("BATCH_NV").build();
batchMapper.insert(batchWithVendor);
batchMapper.insert(batchWithoutVendor);
// 查询:指定 vendorId -> 只匹配有 vendorId 的
MesWmBatchDO query = MesWmBatchDO.builder().itemId(itemId).vendorId(vendorId).build();
MesWmBatchDO result = batchMapper.selectFirst(query);
assertNotNull(result);
assertEquals("BATCH_V1", result.getCode());
assertEquals(vendorId, result.getVendorId());
}
// ==================== selectByCode ====================
@Test
public void testSelectByCode() {
// 准备数据
MesWmBatchDO batch = MesWmBatchDO.builder()
.itemId(randomLongId()).code("TEST_CODE_001").build();
batchMapper.insert(batch);
// 查询
MesWmBatchDO result = batchMapper.selectByCode("TEST_CODE_001");
assertNotNull(result);
assertEquals(batch.getId(), result.getId());
// 查询不存在的
MesWmBatchDO notFound = batchMapper.selectByCode("NOT_EXIST");
assertNull(notFound);
}
// ==================== 向前追溯(集成测试)====================
/**
* 向前追溯集成测试:构造完整链路数据
* 链路:消耗明细(icd, batchCode=RAW_BATCH) → 消耗单(ic) → 报工(pf) → 入库单(pp) → 入库行(ppl, batchCode=PRODUCT_BATCH)
* 预期:查 RAW_BATCH 能找到 PRODUCT_BATCH
*/
@Test
public void testSelectListByForward_withData() {
// 准备:公共 ID
Long workOrderId = randomLongId();
Long rawItemId = randomLongId();
Long productItemId = randomLongId();
Long productBatchId = randomLongId();
LocalDateTime now = LocalDateTime.now();
// 1. 报工记录
MesProFeedbackDO feedback = MesProFeedbackDO.builder()
.workOrderId(workOrderId).itemId(productItemId).build();
feedbackMapper.insert(feedback);
// 2. 消耗单
MesWmItemConsumeDO consume = MesWmItemConsumeDO.builder()
.workOrderId(workOrderId).taskId(randomLongId()).workstationId(randomLongId())
.processId(randomLongId()).feedbackId(feedback.getId()).consumeDate(now).build();
consumeMapper.insert(consume);
// 3. 消耗明细(关联原材料批次 RAW_BATCH
MesWmItemConsumeDetailDO consumeDetail = MesWmItemConsumeDetailDO.builder()
.consumeId(consume.getId()).lineId(randomLongId()).itemId(rawItemId)
.quantity(BigDecimal.TEN).batchCode("RAW_BATCH")
.warehouseId(randomLongId()).locationId(randomLongId()).areaId(randomLongId()).build();
consumeDetailMapper.insert(consumeDetail);
// 4. 生产入库单
MesWmProductProduceDO produce = MesWmProductProduceDO.builder()
.workOrderId(workOrderId).build();
produceMapper.insert(produce);
// 5. 入库行(关联成品批次 PRODUCT_BATCH
MesWmProductProduceLineDO produceLine = MesWmProductProduceLineDO.builder()
.produceId(produce.getId()).itemId(productItemId)
.batchId(productBatchId).batchCode("PRODUCT_BATCH").build();
produceLineMapper.insert(produceLine);
// 执行向前追溯
List<MesWmBatchDO> result = batchMapper.selectListByForward("RAW_BATCH");
// 断言
assertNotNull(result);
assertEquals(1, result.size());
assertEquals("PRODUCT_BATCH", result.get(0).getCode());
assertEquals(productBatchId, result.get(0).getId());
assertEquals(workOrderId, result.get(0).getWorkOrderId());
assertEquals(productItemId, result.get(0).getItemId());
}
/**
* 向前追溯已删除的消耗明细应被过滤icd.deleted = 1
*/
@Test
public void testSelectListByForward_deletedDataFiltered() {
Long workOrderId = randomLongId();
LocalDateTime now = LocalDateTime.now();
// 1. 报工记录
MesProFeedbackDO feedback = MesProFeedbackDO.builder()
.workOrderId(workOrderId).itemId(randomLongId()).build();
feedbackMapper.insert(feedback);
// 2. 消耗单
MesWmItemConsumeDO consume = MesWmItemConsumeDO.builder()
.workOrderId(workOrderId).taskId(randomLongId()).workstationId(randomLongId())
.processId(randomLongId()).feedbackId(feedback.getId()).consumeDate(now).build();
consumeMapper.insert(consume);
// 3. 消耗明细(逻辑删除)
MesWmItemConsumeDetailDO consumeDetail = MesWmItemConsumeDetailDO.builder()
.consumeId(consume.getId()).lineId(randomLongId()).itemId(randomLongId())
.quantity(BigDecimal.TEN).batchCode("RAW_DEL_TEST")
.warehouseId(randomLongId()).locationId(randomLongId()).areaId(randomLongId()).build();
consumeDetailMapper.insert(consumeDetail);
consumeDetailMapper.deleteById(consumeDetail.getId()); // 逻辑删除消耗明细
// 4. 生产入库单 + 入库行
MesWmProductProduceDO produce = MesWmProductProduceDO.builder()
.workOrderId(workOrderId).build();
produceMapper.insert(produce);
MesWmProductProduceLineDO produceLine = MesWmProductProduceLineDO.builder()
.produceId(produce.getId()).itemId(randomLongId())
.batchId(randomLongId()).batchCode("PRODUCT_DEL_TEST").build();
produceLineMapper.insert(produceLine);
// 执行:消耗明细已删除,应该追溯不到
List<MesWmBatchDO> result = batchMapper.selectListByForward("RAW_DEL_TEST");
assertNotNull(result);
assertTrue(result.isEmpty());
}
// ==================== 向后追溯(集成测试)====================
/**
* 向后追溯集成测试:构造完整链路数据
* 链路:入库明细(ppd, batchCode=PRODUCT_BATCH) → 入库单(pp) → 消耗单(ic) → 消耗明细(icd, batchCode=RAW_BATCH)
* 预期:查 PRODUCT_BATCH 能找到 RAW_BATCH
*/
@Test
public void testSelectListByBackward_withData() {
Long workOrderId = randomLongId();
Long rawItemId = randomLongId();
Long rawBatchId = randomLongId();
LocalDateTime now = LocalDateTime.now();
// 1. 生产入库单
MesWmProductProduceDO produce = MesWmProductProduceDO.builder()
.workOrderId(workOrderId).build();
produceMapper.insert(produce);
// 2. 入库明细(关联成品批次 PRODUCT_BACK_BATCH
MesWmProductProduceDetailDO produceDetail = MesWmProductProduceDetailDO.builder()
.produceId(produce.getId()).itemId(randomLongId())
.batchCode("PRODUCT_BACK_BATCH").build();
produceDetailMapper.insert(produceDetail);
// 3. 消耗单
MesWmItemConsumeDO consume = MesWmItemConsumeDO.builder()
.workOrderId(workOrderId).taskId(randomLongId()).workstationId(randomLongId())
.processId(randomLongId()).feedbackId(randomLongId()).consumeDate(now).build();
consumeMapper.insert(consume);
// 4. 消耗明细(关联原材料批次 RAW_BACK_BATCH
MesWmItemConsumeDetailDO consumeDetail = MesWmItemConsumeDetailDO.builder()
.consumeId(consume.getId()).lineId(randomLongId()).itemId(rawItemId)
.quantity(BigDecimal.ONE).batchId(rawBatchId).batchCode("RAW_BACK_BATCH")
.warehouseId(randomLongId()).locationId(randomLongId()).areaId(randomLongId()).build();
consumeDetailMapper.insert(consumeDetail);
// 执行向后追溯
List<MesWmBatchDO> result = batchMapper.selectListByBackward("PRODUCT_BACK_BATCH");
// 断言
assertNotNull(result);
assertEquals(1, result.size());
assertEquals("RAW_BACK_BATCH", result.get(0).getCode());
assertEquals(rawBatchId, result.get(0).getId());
assertEquals(workOrderId, result.get(0).getWorkOrderId());
assertEquals(rawItemId, result.get(0).getItemId());
}
/**
* 向后追溯:消耗明细 batchCode 为 NULL 的应被过滤
*/
@Test
public void testSelectListByBackward_nullBatchCodeFiltered() {
Long workOrderId = randomLongId();
LocalDateTime now = LocalDateTime.now();
// 生产入库单 + 入库明细
MesWmProductProduceDO produce = MesWmProductProduceDO.builder()
.workOrderId(workOrderId).build();
produceMapper.insert(produce);
MesWmProductProduceDetailDO produceDetail = MesWmProductProduceDetailDO.builder()
.produceId(produce.getId()).itemId(randomLongId())
.batchCode("PRODUCT_NULL_TEST").build();
produceDetailMapper.insert(produceDetail);
// 消耗单 + 消耗明细batchCode 为 null
MesWmItemConsumeDO consume = MesWmItemConsumeDO.builder()
.workOrderId(workOrderId).taskId(randomLongId()).workstationId(randomLongId())
.processId(randomLongId()).feedbackId(randomLongId()).consumeDate(now).build();
consumeMapper.insert(consume);
MesWmItemConsumeDetailDO consumeDetail = MesWmItemConsumeDetailDO.builder()
.consumeId(consume.getId()).lineId(randomLongId()).itemId(randomLongId())
.quantity(BigDecimal.ONE).batchCode(null) // 无批次
.warehouseId(randomLongId()).locationId(randomLongId()).areaId(randomLongId()).build();
consumeDetailMapper.insert(consumeDetail);
// 执行:应过滤掉 batchCode IS NULL 的消耗明细
List<MesWmBatchDO> result = batchMapper.selectListByBackward("PRODUCT_NULL_TEST");
assertNotNull(result);
assertTrue(result.isEmpty());
}
}

View File

@@ -0,0 +1,525 @@
package cn.iocoder.yudao.module.mes.service.wm.itemconsume;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockListReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeLineDO;
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.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemconsume.MesWmItemConsumeMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmItemConsumeStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductBomService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteService;
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
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.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_FEEDBACK_ROUTE_PROCESS_INVALID;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_ROUTE_NOT_EXISTS;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* {@link MesWmItemConsumeServiceImpl} 的单元测试
*
* @author 芋道源码
*/
@Import(MesWmItemConsumeServiceImpl.class)
public class MesWmItemConsumeServiceImplTest extends BaseDbUnitTest {
@Resource
private MesWmItemConsumeServiceImpl itemConsumeService;
@Resource
private MesWmItemConsumeMapper itemConsumeMapper;
@MockitoBean
private MesWmItemConsumeLineService itemConsumeLineService;
@MockitoBean
private MesWmItemConsumeDetailService itemConsumeDetailService;
@MockitoBean
private MesProRouteProductBomService routeProductBomService;
@MockitoBean
private MesProRouteService routeService;
@MockitoBean
private MesWmTransactionService wmTransactionService;
@MockitoBean
private MesWmWarehouseService warehouseService;
@MockitoBean
private MesWmWarehouseLocationService locationService;
@MockitoBean
private MesWmWarehouseAreaService areaService;
@MockitoBean
private MesWmMaterialStockService materialStockService;
// ========== 公共 mock 数据 ==========
private static final Long WAREHOUSE_ID = 100L;
private static final Long LOCATION_ID = 200L;
private static final Long AREA_ID = 300L;
private MesWmWarehouseDO virtualWarehouse;
private MesWmWarehouseLocationDO virtualLocation;
private MesWmWarehouseAreaDO virtualArea;
@BeforeEach
public void setUp() {
virtualWarehouse = new MesWmWarehouseDO();
virtualWarehouse.setId(WAREHOUSE_ID);
virtualWarehouse.setCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE);
virtualLocation = new MesWmWarehouseLocationDO();
virtualLocation.setId(LOCATION_ID);
virtualLocation.setCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION);
virtualArea = new MesWmWarehouseAreaDO();
virtualArea.setId(AREA_ID);
virtualArea.setCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA);
when(warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE))
.thenReturn(virtualWarehouse);
when(locationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION))
.thenReturn(virtualLocation);
when(areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA))
.thenReturn(virtualArea);
lenient().when(routeService.validateRouteExists(anyLong())).thenAnswer(invocation -> {
Long routeId = invocation.getArgument(0);
return new MesProRouteDO().setId(routeId);
});
// mock createItemConsumeLineBatch模拟 insertBatch 后填充 ID
AtomicLong lineIdSeq = new AtomicLong(1000L);
doAnswer(invocation -> {
List<MesWmItemConsumeLineDO> lines = invocation.getArgument(0);
for (MesWmItemConsumeLineDO line : lines) {
line.setId(lineIdSeq.getAndIncrement());
}
return null;
}).when(itemConsumeLineService).createItemConsumeLineBatch(any());
}
private MesProFeedbackDO buildFeedback() {
return MesProFeedbackDO.builder()
.id(randomLongId()).workOrderId(randomLongId()).taskId(randomLongId())
.workstationId(randomLongId()).processId(randomLongId()).routeId(randomLongId())
.itemId(randomLongId()).feedbackQuantity(BigDecimal.TEN)
.build();
}
// ========== generateItemConsume ==========
@Test
public void testGenerateItemConsume_routeContextIncomplete() {
MesProFeedbackDO feedback = buildFeedback();
feedback.setRouteId(null);
assertServiceException(() -> itemConsumeService.generateItemConsume(feedback), PRO_FEEDBACK_ROUTE_PROCESS_INVALID);
verify(routeProductBomService, never()).getRouteProductBomList(any(), any(), any());
}
@Test
public void testGenerateItemConsume_routeNotExists() {
MesProFeedbackDO feedback = buildFeedback();
doThrow(ServiceExceptionUtil.exception(PRO_ROUTE_NOT_EXISTS)).when(routeService)
.validateRouteExists(feedback.getRouteId());
assertServiceException(() -> itemConsumeService.generateItemConsume(feedback), PRO_ROUTE_NOT_EXISTS);
verify(routeProductBomService, never()).getRouteProductBomList(any(), any(), any());
}
@Test
public void testGenerateItemConsume_noBom() {
// 准备
MesProFeedbackDO feedback = buildFeedback();
when(routeProductBomService.getRouteProductBomList(
feedback.getRouteId(), feedback.getProcessId(), feedback.getItemId()))
.thenReturn(Collections.emptyList());
// 调用
MesWmItemConsumeDO result = itemConsumeService.generateItemConsume(feedback);
// 断言
assertNull(result);
verify(itemConsumeLineService, never()).createItemConsumeLineBatch(any());
verify(itemConsumeDetailService, never()).createItemConsumeDetailBatch(any());
}
@Test
public void testGenerateItemConsume_noStock() {
// 准备1 个 BOM 物料,线边库无库存
MesProFeedbackDO feedback = buildFeedback();
Long bomItemId = randomLongId();
MesProRouteProductBomDO bom = new MesProRouteProductBomDO();
bom.setItemId(bomItemId);
bom.setQuantity(new BigDecimal("2")); // 用料比例 = 2
when(routeProductBomService.getRouteProductBomList(any(), any(), any()))
.thenReturn(ListUtil.of(bom));
when(materialStockService.getMaterialStockList(any(MesWmMaterialStockListReqVO.class)))
.thenReturn(Collections.emptyList());
// 调用
MesWmItemConsumeDO result = itemConsumeService.generateItemConsume(feedback);
// 断言 1消耗单头已入库
assertNotNull(result);
assertNotNull(result.getId());
assertEquals(MesWmItemConsumeStatusEnum.PREPARE.getStatus(), result.getStatus());
assertEquals(feedback.getId(), result.getFeedbackId());
// 断言 2消耗行正确
@SuppressWarnings("unchecked")
ArgumentCaptor<List<MesWmItemConsumeLineDO>> lineCaptor = ArgumentCaptor.forClass(List.class);
verify(itemConsumeLineService).createItemConsumeLineBatch(lineCaptor.capture());
List<MesWmItemConsumeLineDO> lines = lineCaptor.getValue();
assertEquals(1, lines.size());
assertEquals(bomItemId, lines.get(0).getItemId());
// 消耗数量 = 2 × 10 = 20
assertEquals(0, new BigDecimal("20").compareTo(lines.get(0).getQuantity()));
// 断言 3生成 1 条无批次的 detail
@SuppressWarnings("unchecked")
ArgumentCaptor<List<MesWmItemConsumeDetailDO>> detailCaptor = ArgumentCaptor.forClass(List.class);
verify(itemConsumeDetailService).createItemConsumeDetailBatch(detailCaptor.capture());
List<MesWmItemConsumeDetailDO> details = detailCaptor.getValue();
assertEquals(1, details.size());
MesWmItemConsumeDetailDO detail = details.get(0);
assertEquals(bomItemId, detail.getItemId());
assertEquals(0, new BigDecimal("20").compareTo(detail.getQuantity()));
assertNull(detail.getMaterialStockId());
assertNull(detail.getBatchId());
assertEquals(WAREHOUSE_ID, detail.getWarehouseId());
assertEquals(LOCATION_ID, detail.getLocationId());
assertEquals(AREA_ID, detail.getAreaId());
}
@Test
public void testGenerateItemConsume_singleBatchSufficient() {
// 准备1 个 BOM 物料,线边库有 1 个批次且库存充足
MesProFeedbackDO feedback = buildFeedback();
Long bomItemId = randomLongId();
MesProRouteProductBomDO bom = new MesProRouteProductBomDO();
bom.setItemId(bomItemId);
bom.setQuantity(new BigDecimal("3")); // 消耗 = 3 × 10 = 30
when(routeProductBomService.getRouteProductBomList(any(), any(), any()))
.thenReturn(ListUtil.of(bom));
Long stockId = randomLongId();
Long batchId = randomLongId();
MesWmMaterialStockDO stock = new MesWmMaterialStockDO();
stock.setId(stockId);
stock.setItemId(bomItemId);
stock.setQuantity(new BigDecimal("50")); // 库存 50 > 需求 30
stock.setBatchId(batchId);
stock.setBatchCode("BATCH-001");
when(materialStockService.getMaterialStockList(any(MesWmMaterialStockListReqVO.class)))
.thenReturn(ListUtil.of(stock));
// 调用
MesWmItemConsumeDO result = itemConsumeService.generateItemConsume(feedback);
// 断言:生成 1 条带批次的 detail
assertNotNull(result);
@SuppressWarnings("unchecked")
ArgumentCaptor<List<MesWmItemConsumeDetailDO>> detailCaptor = ArgumentCaptor.forClass(List.class);
verify(itemConsumeDetailService).createItemConsumeDetailBatch(detailCaptor.capture());
List<MesWmItemConsumeDetailDO> details = detailCaptor.getValue();
assertEquals(1, details.size());
MesWmItemConsumeDetailDO detail = details.get(0);
assertEquals(0, new BigDecimal("30").compareTo(detail.getQuantity()));
assertEquals(stockId, detail.getMaterialStockId());
assertEquals(batchId, detail.getBatchId());
assertEquals("BATCH-001", detail.getBatchCode());
}
@Test
public void testGenerateItemConsume_multiBatchFifo() {
// 准备1 个 BOM 物料,需消耗 30线边库有 2 个批次20 + 15
MesProFeedbackDO feedback = buildFeedback();
Long bomItemId = randomLongId();
MesProRouteProductBomDO bom = new MesProRouteProductBomDO();
bom.setItemId(bomItemId);
bom.setQuantity(new BigDecimal("3")); // 消耗 = 3 × 10 = 30
when(routeProductBomService.getRouteProductBomList(any(), any(), any()))
.thenReturn(ListUtil.of(bom));
Long stockId1 = randomLongId();
Long stockId2 = randomLongId();
MesWmMaterialStockDO stock1 = new MesWmMaterialStockDO();
stock1.setId(stockId1);
stock1.setItemId(bomItemId);
stock1.setQuantity(new BigDecimal("20")); // 先进先出:先消耗这批
stock1.setBatchId(1L);
stock1.setBatchCode("BATCH-001");
MesWmMaterialStockDO stock2 = new MesWmMaterialStockDO();
stock2.setId(stockId2);
stock2.setItemId(bomItemId);
stock2.setQuantity(new BigDecimal("15"));
stock2.setBatchId(2L);
stock2.setBatchCode("BATCH-002");
// FIFO 顺序返回
when(materialStockService.getMaterialStockList(any(MesWmMaterialStockListReqVO.class)))
.thenReturn(Arrays.asList(stock1, stock2));
// 调用
MesWmItemConsumeDO result = itemConsumeService.generateItemConsume(feedback);
// 断言:生成 2 条带批次的 detail20 + 10
assertNotNull(result);
@SuppressWarnings("unchecked")
ArgumentCaptor<List<MesWmItemConsumeDetailDO>> detailCaptor = ArgumentCaptor.forClass(List.class);
verify(itemConsumeDetailService).createItemConsumeDetailBatch(detailCaptor.capture());
List<MesWmItemConsumeDetailDO> details = detailCaptor.getValue();
assertEquals(2, details.size());
// 第 1 条:批次 1 全部消耗 20
assertEquals(stockId1, details.get(0).getMaterialStockId());
assertEquals(0, new BigDecimal("20").compareTo(details.get(0).getQuantity()));
assertEquals("BATCH-001", details.get(0).getBatchCode());
// 第 2 条:批次 2 消耗 1030 - 20 = 10
assertEquals(stockId2, details.get(1).getMaterialStockId());
assertEquals(0, new BigDecimal("10").compareTo(details.get(1).getQuantity()));
assertEquals("BATCH-002", details.get(1).getBatchCode());
}
@Test
public void testGenerateItemConsume_insufficientStock() {
// 准备:需消耗 30线边库只有 1 个批次(库存 18不足部分生成无批次 detail
MesProFeedbackDO feedback = buildFeedback();
Long bomItemId = randomLongId();
MesProRouteProductBomDO bom = new MesProRouteProductBomDO();
bom.setItemId(bomItemId);
bom.setQuantity(new BigDecimal("3")); // 消耗 = 3 × 10 = 30
when(routeProductBomService.getRouteProductBomList(any(), any(), any()))
.thenReturn(ListUtil.of(bom));
Long stockId = randomLongId();
MesWmMaterialStockDO stock = new MesWmMaterialStockDO();
stock.setId(stockId);
stock.setItemId(bomItemId);
stock.setQuantity(new BigDecimal("18")); // 不够 30
stock.setBatchId(1L);
stock.setBatchCode("BATCH-001");
when(materialStockService.getMaterialStockList(any(MesWmMaterialStockListReqVO.class)))
.thenReturn(ListUtil.of(stock));
// 调用
MesWmItemConsumeDO result = itemConsumeService.generateItemConsume(feedback);
// 断言2 条 detail18 带批次 + 12 无批次)
assertNotNull(result);
@SuppressWarnings("unchecked")
ArgumentCaptor<List<MesWmItemConsumeDetailDO>> detailCaptor = ArgumentCaptor.forClass(List.class);
verify(itemConsumeDetailService).createItemConsumeDetailBatch(detailCaptor.capture());
List<MesWmItemConsumeDetailDO> details = detailCaptor.getValue();
assertEquals(2, details.size());
// 第 1 条:批次全部消耗 18
assertEquals(stockId, details.get(0).getMaterialStockId());
assertEquals(0, new BigDecimal("18").compareTo(details.get(0).getQuantity()));
assertEquals("BATCH-001", details.get(0).getBatchCode());
// 第 2 条:剩余 12 无批次
assertNull(details.get(1).getMaterialStockId());
assertEquals(0, new BigDecimal("12").compareTo(details.get(1).getQuantity()));
assertNull(details.get(1).getBatchId());
}
@Test
public void testGenerateItemConsume_multipleBomItems() {
// 准备2 个 BOM 物料
MesProFeedbackDO feedback = buildFeedback();
Long itemA = randomLongId();
Long itemB = randomLongId();
MesProRouteProductBomDO bomA = new MesProRouteProductBomDO();
bomA.setItemId(itemA);
bomA.setQuantity(new BigDecimal("1")); // 消耗 = 1 × 10 = 10
MesProRouteProductBomDO bomB = new MesProRouteProductBomDO();
bomB.setItemId(itemB);
bomB.setQuantity(new BigDecimal("2")); // 消耗 = 2 × 10 = 20
when(routeProductBomService.getRouteProductBomList(any(), any(), any()))
.thenReturn(Arrays.asList(bomA, bomB));
// 物料 A 有库存,物料 B 无库存
MesWmMaterialStockDO stockA = new MesWmMaterialStockDO();
stockA.setId(randomLongId());
stockA.setItemId(itemA);
stockA.setQuantity(new BigDecimal("100"));
stockA.setBatchId(1L);
stockA.setBatchCode("BATCH-A");
when(materialStockService.getMaterialStockList(argThat((MesWmMaterialStockListReqVO req) ->
req != null && itemA.equals(req.getItemId()))))
.thenReturn(ListUtil.of(stockA));
when(materialStockService.getMaterialStockList(argThat((MesWmMaterialStockListReqVO req) ->
req != null && itemB.equals(req.getItemId()))))
.thenReturn(Collections.emptyList());
// 调用
MesWmItemConsumeDO result = itemConsumeService.generateItemConsume(feedback);
// 断言2 条消耗行2 条 detail
assertNotNull(result);
@SuppressWarnings("unchecked")
ArgumentCaptor<List<MesWmItemConsumeLineDO>> lineCaptor = ArgumentCaptor.forClass(List.class);
verify(itemConsumeLineService).createItemConsumeLineBatch(lineCaptor.capture());
assertEquals(2, lineCaptor.getValue().size());
@SuppressWarnings("unchecked")
ArgumentCaptor<List<MesWmItemConsumeDetailDO>> detailCaptor = ArgumentCaptor.forClass(List.class);
verify(itemConsumeDetailService).createItemConsumeDetailBatch(detailCaptor.capture());
List<MesWmItemConsumeDetailDO> details = detailCaptor.getValue();
assertEquals(2, details.size());
// 物料 A有批次
MesWmItemConsumeDetailDO detailA = CollUtil.findOne(details,
d -> itemA.equals(d.getItemId()));
assertEquals(0, new BigDecimal("10").compareTo(detailA.getQuantity()));
assertNotNull(detailA.getMaterialStockId());
assertEquals("BATCH-A", detailA.getBatchCode());
// 物料 B无批次
MesWmItemConsumeDetailDO detailB = CollUtil.findOne(details,
d -> itemB.equals(d.getItemId()));
assertEquals(0, new BigDecimal("20").compareTo(detailB.getQuantity()));
assertNull(detailB.getMaterialStockId());
}
// ========== finishItemConsume ==========
@Test
public void testFinishItemConsume_withDetails() {
// 准备:插入一条消耗记录
MesWmItemConsumeDO consume = MesWmItemConsumeDO.builder()
.workOrderId(randomLongId()).taskId(randomLongId())
.workstationId(randomLongId()).processId(randomLongId())
.feedbackId(randomLongId()).consumeDate(java.time.LocalDateTime.now())
.status(MesWmItemConsumeStatusEnum.PREPARE.getStatus()).build();
itemConsumeMapper.insert(consume);
// mock detail 列表
Long lineId1 = randomLongId();
Long lineId2 = randomLongId();
MesWmItemConsumeDetailDO detail1 = MesWmItemConsumeDetailDO.builder()
.consumeId(consume.getId()).lineId(lineId1).itemId(randomLongId())
.quantity(new BigDecimal("20")).batchId(1L).batchCode("BATCH-001")
.warehouseId(WAREHOUSE_ID).locationId(LOCATION_ID).areaId(AREA_ID).build();
MesWmItemConsumeDetailDO detail2 = MesWmItemConsumeDetailDO.builder()
.consumeId(consume.getId()).lineId(lineId2).itemId(randomLongId())
.quantity(new BigDecimal("12"))
.warehouseId(WAREHOUSE_ID).locationId(LOCATION_ID).areaId(AREA_ID).build();
when(itemConsumeDetailService.getItemConsumeDetailListByConsumeId(consume.getId()))
.thenReturn(Arrays.asList(detail1, detail2));
// 调用
itemConsumeService.finishItemConsume(consume.getId());
// 断言 1状态更新为已完成
MesWmItemConsumeDO updated = itemConsumeMapper.selectById(consume.getId());
assertEquals(MesWmItemConsumeStatusEnum.FINISHED.getStatus(), updated.getStatus());
// 断言 2创建了 2 次库存事务
ArgumentCaptor<MesWmTransactionSaveReqDTO> txCaptor = ArgumentCaptor.forClass(MesWmTransactionSaveReqDTO.class);
verify(wmTransactionService, times(2)).createTransaction(txCaptor.capture());
List<MesWmTransactionSaveReqDTO> txList = txCaptor.getAllValues();
// 验证第 1 条事务
MesWmTransactionSaveReqDTO tx1 = txList.get(0);
assertEquals(MesWmTransactionTypeEnum.OUT.getType(), tx1.getType());
assertEquals(detail1.getItemId(), tx1.getItemId());
assertEquals(0, new BigDecimal("-20").compareTo(tx1.getQuantity()));
assertEquals(1L, tx1.getBatchId());
assertEquals("BATCH-001", tx1.getBatchCode());
assertEquals(WAREHOUSE_ID, tx1.getWarehouseId());
assertFalse(tx1.getCheckFlag());
assertEquals(MesBizTypeConstants.WM_ITEM_CONSUME, tx1.getBizType());
assertEquals(consume.getId(), tx1.getBizId());
assertEquals(lineId1, tx1.getBizLineId());
// 验证第 2 条事务(无批次)
MesWmTransactionSaveReqDTO tx2 = txList.get(1);
assertEquals(detail2.getItemId(), tx2.getItemId());
assertEquals(0, new BigDecimal("-12").compareTo(tx2.getQuantity()));
assertNull(tx2.getBatchId());
}
@Test
public void testFinishItemConsume_emptyDetails() {
// 准备:插入一条消耗记录,但没有 detail
MesWmItemConsumeDO consume = MesWmItemConsumeDO.builder()
.workOrderId(randomLongId()).taskId(randomLongId())
.workstationId(randomLongId()).processId(randomLongId())
.feedbackId(randomLongId()).consumeDate(java.time.LocalDateTime.now())
.status(MesWmItemConsumeStatusEnum.PREPARE.getStatus()).build();
itemConsumeMapper.insert(consume);
when(itemConsumeDetailService.getItemConsumeDetailListByConsumeId(consume.getId()))
.thenReturn(Collections.emptyList());
// 调用
itemConsumeService.finishItemConsume(consume.getId());
// 断言:状态更新为已完成,但无事务创建
MesWmItemConsumeDO updated = itemConsumeMapper.selectById(consume.getId());
assertEquals(MesWmItemConsumeStatusEnum.FINISHED.getStatus(), updated.getStatus());
verify(wmTransactionService, never()).createTransaction(any());
}
// ========== getByFeedbackId ==========
@Test
public void testGetByFeedbackId_exists() {
Long feedbackId = randomLongId();
MesWmItemConsumeDO consume = MesWmItemConsumeDO.builder()
.workOrderId(randomLongId()).taskId(randomLongId())
.workstationId(randomLongId()).processId(randomLongId())
.feedbackId(feedbackId).consumeDate(java.time.LocalDateTime.now())
.status(MesWmItemConsumeStatusEnum.PREPARE.getStatus()).build();
itemConsumeMapper.insert(consume);
MesWmItemConsumeDO result = itemConsumeService.getByFeedbackId(feedbackId);
assertNotNull(result);
assertEquals(consume.getId(), result.getId());
}
@Test
public void testGetByFeedbackId_notExists() {
MesWmItemConsumeDO result = itemConsumeService.getByFeedbackId(randomLongId());
assertNull(result);
}
}

View File

@@ -0,0 +1,160 @@
package cn.iocoder.yudao.module.mes.service.wm.itemreceipt;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptDetailDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemreceipt.MesWmItemReceiptMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmItemReceiptStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.vendor.MesMdVendorService;
import cn.iocoder.yudao.module.mes.service.qc.iqc.MesQcIqcService;
import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import jakarta.annotation.Resource;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
/**
* {@link MesWmItemReceiptServiceImpl} 的单元测试
*
* @author 芋道源码
*/
@Import(MesWmItemReceiptServiceImpl.class)
public class MesWmItemReceiptServiceImplTest extends BaseDbUnitTest {
@Resource
private MesWmItemReceiptServiceImpl itemReceiptService;
@Resource
private MesWmItemReceiptMapper itemReceiptMapper;
@MockitoBean
private MesWmItemReceiptLineService itemReceiptLineService;
@MockitoBean
private MesWmItemReceiptDetailService itemReceiptDetailService;
@MockitoBean
private MesWmArrivalNoticeService arrivalNoticeService;
@MockitoBean
private MesMdVendorService vendorService;
@MockitoBean
private MesQcIqcService iqcService;
@MockitoBean
private MesWmTransactionService wmTransactionService;
// ========== finishItemReceipt ==========
@Test
public void testFinishItemReceipt_success_withNoticeId() {
// mock 数据:插入一条待入库状态的入库单(有关联到货通知单)
Long noticeId = randomLongId();
MesWmItemReceiptDO receipt = randomPojo(MesWmItemReceiptDO.class, o -> {
o.setStatus(MesWmItemReceiptStatusEnum.APPROVED.getStatus());
o.setNoticeId(noticeId);
});
itemReceiptMapper.insert(receipt);
Long receiptId = receipt.getId();
// mock 明细列表
MesWmItemReceiptDetailDO detail1 = randomPojo(MesWmItemReceiptDetailDO.class, o -> {
o.setReceiptId(receiptId);
o.setQuantity(BigDecimal.valueOf(100));
});
MesWmItemReceiptDetailDO detail2 = randomPojo(MesWmItemReceiptDetailDO.class, o -> {
o.setReceiptId(receiptId);
o.setQuantity(BigDecimal.valueOf(50));
});
when(itemReceiptDetailService.getItemReceiptDetailListByReceiptId(eq(receiptId)))
.thenReturn(Arrays.asList(detail1, detail2));
// 调用
itemReceiptService.finishItemReceipt(receiptId);
// 断言 1状态更新为已完成
MesWmItemReceiptDO updated = itemReceiptMapper.selectById(receiptId);
assertEquals(MesWmItemReceiptStatusEnum.FINISHED.getStatus(), updated.getStatus());
// 断言 2库存事务被创建2 条明细)
verify(wmTransactionService).createTransactionList(anyList());
// 断言 3到货通知单被完成
verify(arrivalNoticeService).finishArrivalNotice(eq(noticeId));
}
@Test
public void testFinishItemReceipt_success_withoutNoticeId() {
// mock 数据:插入一条待入库状态的入库单(无关联到货通知单)
MesWmItemReceiptDO receipt = randomPojo(MesWmItemReceiptDO.class, o -> {
o.setStatus(MesWmItemReceiptStatusEnum.APPROVED.getStatus());
o.setNoticeId(null); // 无到货通知单
});
itemReceiptMapper.insert(receipt);
Long receiptId = receipt.getId();
// mock 空明细
when(itemReceiptDetailService.getItemReceiptDetailListByReceiptId(eq(receiptId)))
.thenReturn(Collections.emptyList());
// 调用
itemReceiptService.finishItemReceipt(receiptId);
// 断言 1状态更新为已完成
MesWmItemReceiptDO updated = itemReceiptMapper.selectById(receiptId);
assertEquals(MesWmItemReceiptStatusEnum.FINISHED.getStatus(), updated.getStatus());
// 断言 2库存事务仍被调用空列表
verify(wmTransactionService).createTransactionList(anyList());
// 断言 3到货通知单完成方法不被调用
verify(arrivalNoticeService, never()).finishArrivalNotice(any());
}
@Test
public void testFinishItemReceipt_notExists() {
// 准备参数
Long receiptId = randomLongId();
// 调用,并断言异常
assertServiceException(
() -> itemReceiptService.finishItemReceipt(receiptId),
WM_ITEM_RECEIPT_NOT_EXISTS);
}
@Test
public void testFinishItemReceipt_statusNotApproved() {
// mock 数据:插入一条草稿状态
MesWmItemReceiptDO receipt = randomPojo(MesWmItemReceiptDO.class, o -> {
o.setStatus(MesWmItemReceiptStatusEnum.PREPARE.getStatus());
});
itemReceiptMapper.insert(receipt);
// 调用,并断言异常
assertServiceException(
() -> itemReceiptService.finishItemReceipt(receipt.getId()),
WM_ITEM_RECEIPT_STATUS_ERROR);
}
@Test
public void testFinishItemReceipt_statusAlreadyFinished() {
// mock 数据:插入一条已完成状态
MesWmItemReceiptDO receipt = randomPojo(MesWmItemReceiptDO.class, o -> {
o.setStatus(MesWmItemReceiptStatusEnum.FINISHED.getStatus());
});
itemReceiptMapper.insert(receipt);
// 调用,并断言异常
assertServiceException(
() -> itemReceiptService.finishItemReceipt(receipt.getId()),
WM_ITEM_RECEIPT_STATUS_ERROR);
}
}

View File

@@ -0,0 +1,110 @@
package cn.iocoder.yudao.module.mes.service.wm.materialstock;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
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 cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.function.Consumer;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
/**
* {@link MesWmMaterialStockServiceImpl} 的单元测试
*
* @author 芋道源码
*/
@Import(MesWmMaterialStockServiceImpl.class)
public class MesWmMaterialStockServiceImplTest extends BaseDbUnitTest {
@Resource
private MesWmMaterialStockServiceImpl materialStockService;
@Resource
private MesWmMaterialStockMapper materialStockMapper;
@MockitoBean
private MesMdItemService itemService;
@MockitoBean
private MesMdItemTypeService itemTypeService;
@MockitoBean
private MesWmWarehouseAreaService areaService;
@MockitoBean
private MesWmWarehouseService warehouseService;
@Test
public void testGetMaterialStockPage_virtualFilterOnly() {
// 准备数据:虚拟仓库存 + 普通仓库存
Long virtualWarehouseId = 100L;
Long normalWarehouseId = 200L;
MesWmMaterialStockDO virtualStock = createMaterialStockPojo(o -> o.setWarehouseId(virtualWarehouseId));
MesWmMaterialStockDO normalStock = createMaterialStockPojo(o -> o.setWarehouseId(normalWarehouseId));
materialStockMapper.insert(virtualStock);
materialStockMapper.insert(normalStock);
mockVirtualWarehouse(virtualWarehouseId);
// 调用:只看虚拟仓
MesWmMaterialStockPageReqVO reqVO = new MesWmMaterialStockPageReqVO();
reqVO.setVirtualFilter(MesWmMaterialStockPageReqVO.VIRTUAL_FILTER_ONLY);
PageResult<MesWmMaterialStockDO> result = materialStockService.getMaterialStockPage(reqVO);
// 断言:只返回虚拟仓库存,且 total 与列表一致
assertEquals(1, result.getTotal());
assertEquals(1, result.getList().size());
assertEquals(virtualStock.getId(), result.getList().get(0).getId());
}
@Test
public void testGetMaterialStockPage_virtualFilterExclude() {
// 准备数据:虚拟仓库存 + 普通仓库存
Long virtualWarehouseId = 100L;
Long normalWarehouseId = 200L;
MesWmMaterialStockDO virtualStock = createMaterialStockPojo(o -> o.setWarehouseId(virtualWarehouseId));
MesWmMaterialStockDO normalStock = createMaterialStockPojo(o -> o.setWarehouseId(normalWarehouseId));
materialStockMapper.insert(virtualStock);
materialStockMapper.insert(normalStock);
mockVirtualWarehouse(virtualWarehouseId);
// 调用:排除虚拟仓
MesWmMaterialStockPageReqVO reqVO = new MesWmMaterialStockPageReqVO();
reqVO.setVirtualFilter(MesWmMaterialStockPageReqVO.VIRTUAL_FILTER_EXCLUDE);
PageResult<MesWmMaterialStockDO> result = materialStockService.getMaterialStockPage(reqVO);
// 断言:只返回普通仓库存,且 total 与列表一致
assertEquals(1, result.getTotal());
assertEquals(1, result.getList().size());
assertEquals(normalStock.getId(), result.getList().get(0).getId());
}
private MesWmMaterialStockDO createMaterialStockPojo(Consumer<MesWmMaterialStockDO> consumer) {
return randomPojo(MesWmMaterialStockDO.class, o -> {
o.setQuantity(new BigDecimal("10.00"));
o.setReceiptTime(LocalDateTime.of(2026, 1, 1, 0, 0));
o.setFrozen(false);
o.setDeleted(false);
consumer.accept(o);
});
}
private void mockVirtualWarehouse(Long virtualWarehouseId) {
MesWmWarehouseDO virtualWarehouse = new MesWmWarehouseDO();
virtualWarehouse.setId(virtualWarehouseId);
when(warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE))
.thenReturn(virtualWarehouse);
}
}

View File

@@ -0,0 +1,195 @@
package cn.iocoder.yudao.module.mes.service.wm.outsourcereceipt;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.outsourcereceipt.MesWmOutsourceReceiptDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.outsourcereceipt.MesWmOutsourceReceiptLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.outsourcereceipt.MesWmOutsourceReceiptMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmOutsourceReceiptStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.vendor.MesMdVendorService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import jakarta.annotation.Resource;
import java.math.BigDecimal;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* {@link MesWmOutsourceReceiptServiceImpl} 的单元测试
*
* @author 芋道源码
*/
@Import(MesWmOutsourceReceiptServiceImpl.class)
public class MesWmOutsourceReceiptServiceImplTest extends BaseDbUnitTest {
@Resource
private MesWmOutsourceReceiptServiceImpl outsourceReceiptService;
@Resource
private MesWmOutsourceReceiptMapper outsourceReceiptMapper;
@MockitoBean
private MesWmOutsourceReceiptLineService outsourceReceiptLineService;
@MockitoBean
private MesWmOutsourceReceiptDetailService outsourceReceiptDetailService;
@MockitoBean
private MesWmTransactionService wmTransactionService;
@MockitoBean
private MesMdVendorService vendorService;
@MockitoBean
private MesMdItemService itemService;
@MockitoBean
private MesProWorkOrderService workOrderService;
@Test
public void testUpdateOutsourceReceiptWhenIqcFinish_success_bothQualifiedAndUnqualified() {
// mock 数据:插入一条待检验状态的外协入库单
MesWmOutsourceReceiptDO receipt = randomPojo(MesWmOutsourceReceiptDO.class, o -> {
o.setStatus(MesWmOutsourceReceiptStatusEnum.CONFIRMED.getStatus());
});
outsourceReceiptMapper.insert(receipt);
// 准备参数
Long receiptId = receipt.getId();
Long lineId = randomLongId();
Long iqcId = randomLongId();
BigDecimal qualifiedQuantity = BigDecimal.valueOf(80);
BigDecimal unqualifiedQuantity = BigDecimal.valueOf(20);
// mock 行查询
MesWmOutsourceReceiptLineDO line = randomPojo(MesWmOutsourceReceiptLineDO.class, o -> {
o.setId(lineId);
o.setReceiptId(receiptId);
o.setQuantity(BigDecimal.valueOf(100));
});
when(outsourceReceiptLineService.getOutsourceReceiptLine(eq(lineId))).thenReturn(line);
// 调用
outsourceReceiptService.updateOutsourceReceiptWhenIqcFinish(
receiptId, lineId, iqcId, qualifiedQuantity, unqualifiedQuantity);
// 断言:合格品行更新
verify(outsourceReceiptLineService).updateOutsourceReceiptLineDO(argThat(updateLine ->
lineId.equals(updateLine.getId())
&& MesWmQualityStatusEnum.PASS.getStatus().equals(updateLine.getQualityStatus())
&& iqcId.equals(updateLine.getIqcId())
&& qualifiedQuantity.equals(updateLine.getQuantity())));
// 断言:不合格品新增行
verify(outsourceReceiptLineService).createOutsourceReceiptLineDO(argThat(newLine ->
newLine.getId() == null
&& MesWmQualityStatusEnum.FAIL.getStatus().equals(newLine.getQualityStatus())
&& iqcId.equals(newLine.getIqcId())
&& unqualifiedQuantity.equals(newLine.getQuantity())));
// 断言:主表状态更新为待上架
MesWmOutsourceReceiptDO updatedReceipt = outsourceReceiptMapper.selectById(receiptId);
assertEquals(MesWmOutsourceReceiptStatusEnum.APPROVING.getStatus(), updatedReceipt.getStatus());
}
@Test
public void testUpdateOutsourceReceiptWhenIqcFinish_success_onlyQualified() {
// mock 数据:插入一条待检验状态的外协入库单
MesWmOutsourceReceiptDO receipt = randomPojo(MesWmOutsourceReceiptDO.class, o -> {
o.setStatus(MesWmOutsourceReceiptStatusEnum.CONFIRMED.getStatus());
});
outsourceReceiptMapper.insert(receipt);
// 准备参数
Long receiptId = receipt.getId();
Long lineId = randomLongId();
Long iqcId = randomLongId();
BigDecimal qualifiedQuantity = BigDecimal.valueOf(100);
BigDecimal unqualifiedQuantity = BigDecimal.ZERO; // 没有不合格品
// mock 行查询
MesWmOutsourceReceiptLineDO line = randomPojo(MesWmOutsourceReceiptLineDO.class, o -> {
o.setId(lineId);
o.setReceiptId(receiptId);
});
when(outsourceReceiptLineService.getOutsourceReceiptLine(eq(lineId))).thenReturn(line);
// 调用
outsourceReceiptService.updateOutsourceReceiptWhenIqcFinish(
receiptId, lineId, iqcId, qualifiedQuantity, unqualifiedQuantity);
// 断言:合格品行更新
verify(outsourceReceiptLineService).updateOutsourceReceiptLineDO(argThat(updateLine ->
lineId.equals(updateLine.getId())));
// 断言:不合格品不应该新增行(数量为 0
verify(outsourceReceiptLineService, never()).createOutsourceReceiptLineDO(any());
// 断言:主表状态更新为待上架
MesWmOutsourceReceiptDO updatedReceipt = outsourceReceiptMapper.selectById(receiptId);
assertEquals(MesWmOutsourceReceiptStatusEnum.APPROVING.getStatus(), updatedReceipt.getStatus());
}
@Test
public void testUpdateOutsourceReceiptWhenIqcFinish_receiptNotExists() {
// 准备参数
Long receiptId = randomLongId();
Long lineId = randomLongId();
Long iqcId = randomLongId();
BigDecimal qualifiedQuantity = BigDecimal.valueOf(100);
BigDecimal unqualifiedQuantity = BigDecimal.ZERO;
// 调用,并断言异常
assertServiceException(
() -> outsourceReceiptService.updateOutsourceReceiptWhenIqcFinish(
receiptId, lineId, iqcId, qualifiedQuantity, unqualifiedQuantity),
WM_OUTSOURCE_RECEIPT_NOT_EXISTS);
}
@Test
public void testUpdateOutsourceReceiptWhenIqcFinish_statusNotConfirmed() {
// mock 数据:插入一条草稿状态的外协入库单(非待检验状态)
MesWmOutsourceReceiptDO receipt = randomPojo(MesWmOutsourceReceiptDO.class, o -> {
o.setStatus(MesWmOutsourceReceiptStatusEnum.PREPARE.getStatus());
});
outsourceReceiptMapper.insert(receipt);
// 准备参数
Long receiptId = receipt.getId();
Long lineId = randomLongId();
Long iqcId = randomLongId();
BigDecimal qualifiedQuantity = BigDecimal.valueOf(100);
BigDecimal unqualifiedQuantity = BigDecimal.ZERO;
// 调用,并断言异常
assertServiceException(
() -> outsourceReceiptService.updateOutsourceReceiptWhenIqcFinish(
receiptId, lineId, iqcId, qualifiedQuantity, unqualifiedQuantity),
WM_OUTSOURCE_RECEIPT_STATUS_ERROR);
}
@Test
public void testUpdateOutsourceReceiptWhenIqcFinish_lineNotExists() {
// mock 数据:插入一条待检验状态的外协入库单
MesWmOutsourceReceiptDO receipt = randomPojo(MesWmOutsourceReceiptDO.class, o -> {
o.setStatus(MesWmOutsourceReceiptStatusEnum.CONFIRMED.getStatus());
});
outsourceReceiptMapper.insert(receipt);
// 准备参数
Long receiptId = receipt.getId();
Long lineId = randomLongId();
Long iqcId = randomLongId();
BigDecimal qualifiedQuantity = BigDecimal.valueOf(100);
BigDecimal unqualifiedQuantity = BigDecimal.ZERO;
// mock 行查询返回 null
when(outsourceReceiptLineService.getOutsourceReceiptLine(eq(lineId))).thenReturn(null);
// 调用,并断言异常
assertServiceException(
() -> outsourceReceiptService.updateOutsourceReceiptWhenIqcFinish(
receiptId, lineId, iqcId, qualifiedQuantity, unqualifiedQuantity),
WM_OUTSOURCE_RECEIPT_LINE_NOT_EXISTS);
}
}

View File

@@ -0,0 +1,259 @@
package cn.iocoder.yudao.module.mes.service.wm.productproduce;
import cn.hutool.core.collection.ListUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
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.productproduce.MesWmProductProduceMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductProduceStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
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.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.math.BigDecimal;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* {@link MesWmProductProduceServiceImpl} 的单元测试
*
* @author 芋道源码
*/
@Import(MesWmProductProduceServiceImpl.class)
public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest {
@Resource
private MesWmProductProduceServiceImpl productProduceService;
@Resource
private MesWmProductProduceMapper productProduceMapper;
@MockitoBean
private MesWmProductProduceLineService productProduceLineService;
@MockitoBean
private MesWmProductProduceDetailService productProduceDetailService;
@MockitoBean
private MesProWorkOrderService workOrderService;
@MockitoBean
private MesWmBatchService batchService;
@MockitoBean
private MesWmTransactionService wmTransactionService;
@MockitoBean
private MesWmWarehouseService warehouseService;
@MockitoBean
private MesWmWarehouseLocationService locationService;
@MockitoBean
private MesWmWarehouseAreaService areaService;
@Test
public void testSplitPendingAndFinishProduce_withUnqualified() {
// 准备数据:插入一个 PREPARE 状态的产出单
Long feedbackId = randomLongId();
Long sourceLineId = 100L;
Long itemId = randomLongId();
Long batchId = randomLongId();
String batchCode = "BATCH-001";
MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class, o -> {
o.setFeedbackId(feedbackId);
o.setStatus(MesWmProductProduceStatusEnum.PREPARE.getStatus());
});
productProduceMapper.insert(produce);
// mock: 虚拟线边库
Long whId = randomLongId(), locId = randomLongId(), areaId = randomLongId();
MesWmWarehouseDO wh = new MesWmWarehouseDO(); wh.setId(whId);
MesWmWarehouseLocationDO loc = new MesWmWarehouseLocationDO(); loc.setId(locId);
MesWmWarehouseAreaDO area = new MesWmWarehouseAreaDO(); area.setId(areaId);
when(warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE)).thenReturn(wh);
when(locationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION)).thenReturn(loc);
when(areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA)).thenReturn(area);
// mock: 通过 sourceLineId 直接定位 PENDING 产出行
MesWmProductProduceLineDO pendingLine = MesWmProductProduceLineDO.builder()
.id(sourceLineId).produceId(produce.getId()).feedbackId(feedbackId)
.itemId(itemId).quantity(BigDecimal.valueOf(100))
.batchId(batchId).batchCode(batchCode)
.qualityStatus(MesWmQualityStatusEnum.PENDING.getStatus())
.build();
when(productProduceLineService.validateProductProduceLineExists(sourceLineId))
.thenReturn(pendingLine);
// mock: finishProductProduce 内部查行和明细(用于创建库存事务)
MesWmProductProduceLineDO qualifiedLine = MesWmProductProduceLineDO.builder()
.id(100L).produceId(produce.getId()).itemId(itemId)
.quantity(BigDecimal.valueOf(80))
.qualityStatus(MesWmQualityStatusEnum.PASS.getStatus())
.build();
MesWmProductProduceLineDO unqualifiedLine = MesWmProductProduceLineDO.builder()
.id(101L).produceId(produce.getId()).itemId(itemId)
.quantity(BigDecimal.valueOf(20))
.qualityStatus(MesWmQualityStatusEnum.FAIL.getStatus())
.build();
when(productProduceLineService.getProductProduceLineListByProduceId(produce.getId()))
.thenReturn(ListUtil.of(qualifiedLine, unqualifiedLine));
// mock: finishProductProduce 中按行查明细
MesWmProductProduceDetailDO qualifiedDetail = MesWmProductProduceDetailDO.builder()
.lineId(100L).quantity(BigDecimal.valueOf(80)).build();
MesWmProductProduceDetailDO unqualifiedDetail = MesWmProductProduceDetailDO.builder()
.lineId(101L).quantity(BigDecimal.valueOf(20)).build();
when(productProduceDetailService.getProductProduceDetailListByLineId(100L))
.thenReturn(ListUtil.of(qualifiedDetail));
when(productProduceDetailService.getProductProduceDetailListByLineId(101L))
.thenReturn(ListUtil.of(unqualifiedDetail));
// 调用
productProduceService.splitPendingAndFinishProduce(feedbackId, sourceLineId,
BigDecimal.valueOf(80), BigDecimal.valueOf(20));
// 断言 1不合格品行 - 新建了一行
ArgumentCaptor<MesWmProductProduceLineDO> lineCaptor = ArgumentCaptor.forClass(MesWmProductProduceLineDO.class);
verify(productProduceLineService).createProductProduceLine(lineCaptor.capture());
MesWmProductProduceLineDO createdLine = lineCaptor.getValue();
assertEquals(MesWmQualityStatusEnum.FAIL.getStatus(), createdLine.getQualityStatus());
assertEquals(0, BigDecimal.valueOf(20).compareTo(createdLine.getQuantity()));
assertEquals(itemId, createdLine.getItemId());
// 断言 2原待检行 - 更新为合格品
ArgumentCaptor<MesWmProductProduceLineDO> updateCaptor = ArgumentCaptor.forClass(MesWmProductProduceLineDO.class);
verify(productProduceLineService).updateProductProduceLine(updateCaptor.capture());
MesWmProductProduceLineDO updatedLine = updateCaptor.getValue();
assertEquals(MesWmQualityStatusEnum.PASS.getStatus(), updatedLine.getQualityStatus());
assertEquals(0, BigDecimal.valueOf(80).compareTo(updatedLine.getQuantity()));
// 断言 3生成了 2 条明细(不合格品明细 + 合格品明细)
verify(productProduceDetailService, times(2)).createProductProduceDetail(any());
// 断言 4产出单状态更新为已完成
MesWmProductProduceDO updatedProduce = productProduceMapper.selectById(produce.getId());
assertEquals(MesWmProductProduceStatusEnum.FINISHED.getStatus(), updatedProduce.getStatus());
// 断言 5创建了库存事务
verify(wmTransactionService, times(2)).createTransaction(any());
}
@Test
public void testSplitPendingAndFinishProduce_allQualified() {
// 准备数据:全部合格品场景
Long feedbackId = randomLongId();
Long sourceLineId = 200L;
Long itemId = randomLongId();
MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class, o -> {
o.setFeedbackId(feedbackId);
o.setStatus(MesWmProductProduceStatusEnum.PREPARE.getStatus());
});
productProduceMapper.insert(produce);
// mock: 虚拟线边库
Long whId = randomLongId(), locId = randomLongId(), areaId = randomLongId();
MesWmWarehouseDO wh = new MesWmWarehouseDO(); wh.setId(whId);
MesWmWarehouseLocationDO loc = new MesWmWarehouseLocationDO(); loc.setId(locId);
MesWmWarehouseAreaDO area = new MesWmWarehouseAreaDO(); area.setId(areaId);
when(warehouseService.getWarehouseByCode(any())).thenReturn(wh);
when(locationService.getWarehouseLocationByCode(any())).thenReturn(loc);
when(areaService.getWarehouseAreaByCode(any())).thenReturn(area);
// mock: 通过 sourceLineId 直接定位 PENDING 产出行
MesWmProductProduceLineDO pendingLine = MesWmProductProduceLineDO.builder()
.id(sourceLineId).produceId(produce.getId()).feedbackId(feedbackId)
.itemId(itemId).quantity(BigDecimal.valueOf(50))
.qualityStatus(MesWmQualityStatusEnum.PENDING.getStatus())
.build();
when(productProduceLineService.validateProductProduceLineExists(sourceLineId))
.thenReturn(pendingLine);
// mock: finishProductProduce 查行和明细
MesWmProductProduceLineDO qualifiedLine = MesWmProductProduceLineDO.builder()
.id(200L).produceId(produce.getId()).itemId(itemId)
.quantity(BigDecimal.valueOf(50))
.qualityStatus(MesWmQualityStatusEnum.PASS.getStatus())
.build();
when(productProduceLineService.getProductProduceLineListByProduceId(produce.getId()))
.thenReturn(ListUtil.of(qualifiedLine));
MesWmProductProduceDetailDO detail = MesWmProductProduceDetailDO.builder()
.lineId(200L).quantity(BigDecimal.valueOf(50)).build();
when(productProduceDetailService.getProductProduceDetailListByLineId(200L))
.thenReturn(ListUtil.of(detail));
// 调用:不合格品数量 = 0
productProduceService.splitPendingAndFinishProduce(feedbackId, sourceLineId,
BigDecimal.valueOf(50), BigDecimal.ZERO);
// 断言 1没有新建行不合格品数量为 0
verify(productProduceLineService, never()).createProductProduceLine(any());
// 断言 2更新了原待检行为合格
ArgumentCaptor<MesWmProductProduceLineDO> updateCaptor = ArgumentCaptor.forClass(MesWmProductProduceLineDO.class);
verify(productProduceLineService).updateProductProduceLine(updateCaptor.capture());
assertEquals(MesWmQualityStatusEnum.PASS.getStatus(), updateCaptor.getValue().getQualityStatus());
// 断言 3只生成了 1 条明细(全部合格品)
verify(productProduceDetailService, times(1)).createProductProduceDetail(any());
// 断言 4产出单状态更新为已完成
MesWmProductProduceDO updatedProduce = productProduceMapper.selectById(produce.getId());
assertEquals(MesWmProductProduceStatusEnum.FINISHED.getStatus(), updatedProduce.getStatus());
}
@Test
public void testSplitPendingAndFinishProduce_produceNotExists() {
// 调用不存在的 feedbackId应该抛异常
Long feedbackId = randomLongId();
Long sourceLineId = randomLongId();
ServiceException ex = assertThrows(ServiceException.class, () ->
productProduceService.splitPendingAndFinishProduce(feedbackId, sourceLineId,
BigDecimal.TEN, BigDecimal.ZERO));
assertEquals(WM_PRODUCT_PRODUCE_NOT_EXISTS.getCode(), ex.getCode());
}
@Test
public void testSplitPendingAndFinishProduce_lineNotExists() {
// 准备数据:产出单存在,但 sourceLineId 对应的行不存在
Long feedbackId = randomLongId();
Long sourceLineId = randomLongId();
MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class, o -> {
o.setFeedbackId(feedbackId);
o.setStatus(MesWmProductProduceStatusEnum.PREPARE.getStatus());
});
productProduceMapper.insert(produce);
// mock: 虚拟线边库
MesWmWarehouseDO wh = new MesWmWarehouseDO(); wh.setId(1L);
MesWmWarehouseLocationDO loc = new MesWmWarehouseLocationDO(); loc.setId(1L);
MesWmWarehouseAreaDO area = new MesWmWarehouseAreaDO(); area.setId(1L);
when(warehouseService.getWarehouseByCode(any())).thenReturn(wh);
when(locationService.getWarehouseLocationByCode(any())).thenReturn(loc);
when(areaService.getWarehouseAreaByCode(any())).thenReturn(area);
// mock: validateProductProduceLineExists 抛异常
when(productProduceLineService.validateProductProduceLineExists(sourceLineId))
.thenThrow(new ServiceException(WM_PRODUCT_PRODUCE_LINE_NOT_EXISTS));
// 调用,应该抛异常
ServiceException ex = assertThrows(ServiceException.class, () ->
productProduceService.splitPendingAndFinishProduce(feedbackId, sourceLineId,
BigDecimal.TEN, BigDecimal.ZERO));
assertEquals(WM_PRODUCT_PRODUCE_LINE_NOT_EXISTS.getCode(), ex.getCode());
}
}

View File

@@ -0,0 +1,168 @@
package cn.iocoder.yudao.module.mes.service.wm.productsales;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesLineMapper;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcCheckResultEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
/**
* {@link MesWmProductSalesLineServiceImpl} 的单元测试类
*
* @author 芋道源码
*/
@Import(MesWmProductSalesLineServiceImpl.class)
public class MesWmProductSalesLineServiceImplTest extends BaseDbUnitTest {
@Resource
private MesWmProductSalesLineServiceImpl productSalesLineService;
@Resource
private MesWmProductSalesLineMapper productSalesLineMapper;
@MockitoBean
private MesWmProductSalesService productSalesService;
@MockitoBean
private MesWmProductSalesDetailService productSalesDetailService;
@MockitoBean
private MesMdItemService itemService;
@MockitoBean
private MesWmBatchService batchService;
@Test
public void testUpdateProductSalesLineWhenOqcFinish_pass() {
// 准备参数
Long oqcId = randomLongId();
Integer checkResult = MesQcCheckResultEnum.PASS.getType();
// mock 数据
MesWmProductSalesLineDO lineDO = randomPojo(MesWmProductSalesLineDO.class, o -> {
o.setOqcCheckFlag(true); // 需要 OQC 检验
o.setQualityStatus(null); // 初始为空
});
productSalesLineMapper.insert(lineDO);
// 调用
productSalesLineService.updateProductSalesLineWhenOqcFinish(lineDO.getId(), oqcId, checkResult);
// 断言:验证行字段更新
MesWmProductSalesLineDO updateLine = productSalesLineMapper.selectById(lineDO.getId());
assertEquals(oqcId, updateLine.getOqcId());
assertEquals(MesWmQualityStatusEnum.PASS.getStatus(), updateLine.getQualityStatus());
}
@Test
public void testUpdateProductSalesLineWhenOqcFinish_fail() {
// 准备参数
Long oqcId = randomLongId();
Integer checkResult = MesQcCheckResultEnum.FAIL.getType();
// mock 数据
MesWmProductSalesLineDO lineDO = randomPojo(MesWmProductSalesLineDO.class, o -> {
o.setOqcCheckFlag(true);
o.setQualityStatus(null);
});
productSalesLineMapper.insert(lineDO);
// 调用
productSalesLineService.updateProductSalesLineWhenOqcFinish(lineDO.getId(), oqcId, checkResult);
// 断言:验证行字段更新
MesWmProductSalesLineDO updateLine = productSalesLineMapper.selectById(lineDO.getId());
assertEquals(oqcId, updateLine.getOqcId());
assertEquals(MesWmQualityStatusEnum.FAIL.getStatus(), updateLine.getQualityStatus());
}
@Test
public void testUpdateProductSalesLineWhenOqcFinish_cancelWhenNg() {
// 准备数据:同一出库单下 2 行,第 1 行已合格,第 2 行即将变更为不合格
Long salesId = randomLongId();
MesWmProductSalesLineDO line1 = randomPojo(MesWmProductSalesLineDO.class, o -> {
o.setSalesId(salesId);
o.setOqcCheckFlag(true);
o.setQualityStatus(MesWmQualityStatusEnum.PASS.getStatus());
});
productSalesLineMapper.insert(line1);
MesWmProductSalesLineDO line2 = randomPojo(MesWmProductSalesLineDO.class, o -> {
o.setSalesId(salesId);
o.setOqcCheckFlag(true);
o.setQualityStatus(null); // 待检
});
productSalesLineMapper.insert(line2);
// 调用:第 2 行质检不合格
productSalesLineService.updateProductSalesLineWhenOqcFinish(
line2.getId(), randomLongId(), MesQcCheckResultEnum.FAIL.getType());
// 断言:应该调用 cancelProductSales
verify(productSalesService).cancelProductSales(eq(salesId));
}
@Test
public void testUpdateProductSalesLineWhenOqcFinish_noCancelWhenAllPass() {
// 准备数据:同一出库单下 2 行,第 1 行已合格,第 2 行即将变更为合格
Long salesId = randomLongId();
MesWmProductSalesLineDO line1 = randomPojo(MesWmProductSalesLineDO.class, o -> {
o.setSalesId(salesId);
o.setOqcCheckFlag(true);
o.setQualityStatus(MesWmQualityStatusEnum.PASS.getStatus());
});
productSalesLineMapper.insert(line1);
MesWmProductSalesLineDO line2 = randomPojo(MesWmProductSalesLineDO.class, o -> {
o.setSalesId(salesId);
o.setOqcCheckFlag(true);
o.setQualityStatus(null);
});
productSalesLineMapper.insert(line2);
// 调用:第 2 行质检合格
productSalesLineService.updateProductSalesLineWhenOqcFinish(
line2.getId(), randomLongId(), MesQcCheckResultEnum.PASS.getType());
// 断言:不应该调用 cancelProductSales
verify(productSalesService, never()).cancelProductSales(anyLong());
}
@Test
public void testUpdateProductSalesLineWhenOqcFinish_skipNonOqcCheckLines() {
// 准备数据:同一出库单下 2 行,第 1 行不需要 OQC 检验oqcCheckFlag=false第 2 行需要检验且不合格
Long salesId = randomLongId();
MesWmProductSalesLineDO line1 = randomPojo(MesWmProductSalesLineDO.class, o -> {
o.setSalesId(salesId);
o.setOqcCheckFlag(false); // 不需要 OQC 检验
o.setQualityStatus(null);
});
productSalesLineMapper.insert(line1);
MesWmProductSalesLineDO line2 = randomPojo(MesWmProductSalesLineDO.class, o -> {
o.setSalesId(salesId);
o.setOqcCheckFlag(true);
o.setQualityStatus(null);
});
productSalesLineMapper.insert(line2);
// 调用:第 2 行质检不合格
productSalesLineService.updateProductSalesLineWhenOqcFinish(
line2.getId(), randomLongId(), MesQcCheckResultEnum.FAIL.getType());
// 断言:第 2 行不合格 → 应该取消出库单
verify(productSalesService).cancelProductSales(eq(salesId));
}
}

View File

@@ -0,0 +1,114 @@
package cn.iocoder.yudao.module.mes.service.wm.returnissue;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue.MesWmReturnIssueLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.returnissue.MesWmReturnIssueLineMapper;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcCheckResultEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import java.math.BigDecimal;
import java.util.List;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/**
* {@link MesWmReturnIssueLineServiceImpl} 的单元测试类
*
* @author 芋道源码
*/
@Import(MesWmReturnIssueLineServiceImpl.class)
public class MesWmReturnIssueLineServiceImplTest extends BaseDbUnitTest {
@Resource
private MesWmReturnIssueLineServiceImpl returnIssueLineService;
@Resource
private MesWmReturnIssueLineMapper returnIssueLineMapper;
@MockitoBean
private MesWmReturnIssueService issueService;
@MockitoBean
private MesMdItemService itemService;
@Test
public void testUpdateReturnIssueLineWhenRqcFinish_allPass() {
// 准备参数:全部合格
Integer checkResult = MesQcCheckResultEnum.PASS.getType();
BigDecimal qualifiedQty = new BigDecimal("100");
BigDecimal unqualifiedQty = BigDecimal.ZERO;
// mock 数据
MesWmReturnIssueLineDO lineDO = randomPojo(MesWmReturnIssueLineDO.class, o -> o.setQuantity(qualifiedQty));
returnIssueLineMapper.insert(lineDO);
// 调用sourceDocId 传 null 不联动主表状态)
returnIssueLineService.updateReturnIssueLineWhenRqcFinish(
lineDO.getId(), null, checkResult, qualifiedQty, unqualifiedQty);
// 断言:验证质量状态更新为合格
MesWmReturnIssueLineDO updateLine = returnIssueLineMapper.selectById(lineDO.getId());
assertEquals(MesWmQualityStatusEnum.PASS.getStatus(), updateLine.getQualityStatus());
}
@Test
public void testUpdateReturnIssueLineWhenRqcFinish_allFail() {
// 准备参数:全部不合格
Integer checkResult = MesQcCheckResultEnum.FAIL.getType();
BigDecimal qualifiedQty = BigDecimal.ZERO;
BigDecimal unqualifiedQty = new BigDecimal("100");
// mock 数据
MesWmReturnIssueLineDO lineDO = randomPojo(MesWmReturnIssueLineDO.class, o -> o.setQuantity(unqualifiedQty));
returnIssueLineMapper.insert(lineDO);
// 调用
returnIssueLineService.updateReturnIssueLineWhenRqcFinish(
lineDO.getId(), null, checkResult, qualifiedQty, unqualifiedQty);
// 断言:验证质量状态更新为不合格
MesWmReturnIssueLineDO updateLine = returnIssueLineMapper.selectById(lineDO.getId());
assertEquals(MesWmQualityStatusEnum.FAIL.getStatus(), updateLine.getQualityStatus());
}
@Test
public void testUpdateReturnIssueLineWhenRqcFinish_partialSplit() {
// 准备参数:部分合格部分不合格,触发行拆分
Integer checkResult = MesQcCheckResultEnum.PASS.getType();
BigDecimal qualifiedQty = new BigDecimal("80");
BigDecimal unqualifiedQty = new BigDecimal("20");
// mock 数据
MesWmReturnIssueLineDO lineDO = randomPojo(MesWmReturnIssueLineDO.class, o ->
o.setQuantity(new BigDecimal("100")));
returnIssueLineMapper.insert(lineDO);
// 调用
returnIssueLineService.updateReturnIssueLineWhenRqcFinish(
lineDO.getId(), null, checkResult, qualifiedQty, unqualifiedQty);
// 断言:原行变为合格,数量调整为合格数量
MesWmReturnIssueLineDO originalLine = returnIssueLineMapper.selectById(lineDO.getId());
assertEquals(MesWmQualityStatusEnum.PASS.getStatus(), originalLine.getQualityStatus());
assertEquals(0, qualifiedQty.compareTo(originalLine.getQuantity()));
// 断言:新增了一行不合格品
List<MesWmReturnIssueLineDO> allLines = returnIssueLineMapper.selectListByIssueId(lineDO.getIssueId());
assertEquals(2, allLines.size());
MesWmReturnIssueLineDO newLine = allLines.stream()
.filter(l -> !l.getId().equals(lineDO.getId()))
.findFirst().orElse(null);
assertNotNull(newLine);
assertEquals(MesWmQualityStatusEnum.FAIL.getStatus(), newLine.getQualityStatus());
assertEquals(0, unqualifiedQty.compareTo(newLine.getQuantity()));
}
}

View File

@@ -0,0 +1,121 @@
package cn.iocoder.yudao.module.mes.service.wm.returnsales;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.returnsales.MesWmReturnSalesLineMapper;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcCheckResultEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import java.math.BigDecimal;
import java.util.List;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/**
* {@link MesWmReturnSalesLineServiceImpl} 的单元测试类
*
* @author 芋道源码
*/
@Import(MesWmReturnSalesLineServiceImpl.class)
public class MesWmReturnSalesLineServiceImplTest extends BaseDbUnitTest {
@Resource
private MesWmReturnSalesLineServiceImpl returnSalesLineService;
@Resource
private MesWmReturnSalesLineMapper returnSalesLineMapper;
@MockitoBean
private MesWmReturnSalesService returnSalesService;
@MockitoBean
private MesMdItemService itemService;
@MockitoBean
private MesWmBatchService batchService;
@MockitoBean
private MesWmReturnSalesDetailService returnSalesDetailService;
@Test
public void testUpdateReturnSalesLineWhenRqcFinish_allPass() {
// 准备参数:全部合格
Integer checkResult = MesQcCheckResultEnum.PASS.getType();
BigDecimal qualifiedQty = new BigDecimal("100");
BigDecimal unqualifiedQty = BigDecimal.ZERO;
// mock 数据
MesWmReturnSalesLineDO lineDO = randomPojo(MesWmReturnSalesLineDO.class, o -> o.setQuantity(qualifiedQty));
returnSalesLineMapper.insert(lineDO);
// 调用sourceDocId 传 null 不联动主表状态)
returnSalesLineService.updateReturnSalesLineWhenRqcFinish(
lineDO.getId(), null, checkResult, qualifiedQty, unqualifiedQty);
// 断言:验证质量状态更新为合格
MesWmReturnSalesLineDO updateLine = returnSalesLineMapper.selectById(lineDO.getId());
assertEquals(MesWmQualityStatusEnum.PASS.getStatus(), updateLine.getQualityStatus());
}
@Test
public void testUpdateReturnSalesLineWhenRqcFinish_allFail() {
// 准备参数:全部不合格
Integer checkResult = MesQcCheckResultEnum.FAIL.getType();
BigDecimal qualifiedQty = BigDecimal.ZERO;
BigDecimal unqualifiedQty = new BigDecimal("100");
// mock 数据
MesWmReturnSalesLineDO lineDO = randomPojo(MesWmReturnSalesLineDO.class, o -> o.setQuantity(unqualifiedQty));
returnSalesLineMapper.insert(lineDO);
// 调用
returnSalesLineService.updateReturnSalesLineWhenRqcFinish(
lineDO.getId(), null, checkResult, qualifiedQty, unqualifiedQty);
// 断言:验证质量状态更新为不合格
MesWmReturnSalesLineDO updateLine = returnSalesLineMapper.selectById(lineDO.getId());
assertEquals(MesWmQualityStatusEnum.FAIL.getStatus(), updateLine.getQualityStatus());
}
@Test
public void testUpdateReturnSalesLineWhenRqcFinish_partialSplit() {
// 准备参数:部分合格部分不合格,触发行拆分
Integer checkResult = MesQcCheckResultEnum.PASS.getType();
BigDecimal qualifiedQty = new BigDecimal("70");
BigDecimal unqualifiedQty = new BigDecimal("30");
// mock 数据
MesWmReturnSalesLineDO lineDO = randomPojo(MesWmReturnSalesLineDO.class, o ->
o.setQuantity(new BigDecimal("100")));
returnSalesLineMapper.insert(lineDO);
// 调用
returnSalesLineService.updateReturnSalesLineWhenRqcFinish(
lineDO.getId(), null, checkResult, qualifiedQty, unqualifiedQty);
// 断言:原行变为合格,数量调整为合格数量
MesWmReturnSalesLineDO originalLine = returnSalesLineMapper.selectById(lineDO.getId());
assertEquals(MesWmQualityStatusEnum.PASS.getStatus(), originalLine.getQualityStatus());
assertEquals(0, qualifiedQty.compareTo(originalLine.getQuantity()));
// 断言:新增了一行不合格品
List<MesWmReturnSalesLineDO> allLines = returnSalesLineMapper.selectListByReturnId(lineDO.getReturnId());
assertEquals(2, allLines.size());
MesWmReturnSalesLineDO newLine = allLines.stream()
.filter(l -> !l.getId().equals(lineDO.getId()))
.findFirst().orElse(null);
assertNotNull(newLine);
assertEquals(MesWmQualityStatusEnum.FAIL.getStatus(), newLine.getQualityStatus());
assertEquals(0, unqualifiedQty.compareTo(newLine.getQuantity()));
}
}

View File

@@ -0,0 +1,49 @@
spring:
main:
lazy-initialization: true # 开启懒加载,加快速度
banner-mode: off # 单元测试,禁用 Banner
allow-bean-definition-overriding: true # 允许 Bean 覆盖,解决 Redis 配置冲突
--- #################### 数据库相关配置 ####################
spring:
# 数据源配置项
datasource:
name: ruoyi-vue-pro
url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value,day; # MODE 使用 MySQL 模式DATABASE_TO_UPPER 配置表和字段使用小写
driver-class-name: org.h2.Driver
username: sa
password:
druid:
async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度
initial-size: 1 # 单元测试,配置为 1提升启动速度
sql:
init:
schema-locations: classpath:/sql/create_tables.sql
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
data:
redis:
host: 127.0.0.1 # 地址
port: 16379 # 端口(单元测试,使用 16379 端口)
database: 0 # 数据库索引
mybatis:
lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试
--- #################### 定时任务相关配置 ####################
--- #################### 配置中心相关配置 ####################
--- #################### 服务保障相关配置 ####################
# Lock4j 配置项(单元测试,禁用 Lock4j
--- #################### 监控相关配置 ####################
--- #################### 芋道相关配置 ####################
# 芋道配置项,设置当前项目所有自定义的配置
yudao:
info:
base-package: cn.iocoder.yudao.module

View File

@@ -0,0 +1,4 @@
<configuration>
<!-- 引用 Spring Boot 的 logback 基础配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
</configuration>

View File

@@ -0,0 +1,23 @@
DELETE FROM "mes_cal_team_shift";
DELETE FROM "mes_cal_plan_team";
DELETE FROM "mes_cal_plan_shift";
DELETE FROM "mes_cal_plan";
DELETE FROM "mes_md_auto_code_rule";
DELETE FROM "mes_md_auto_code_part";
DELETE FROM "mes_md_auto_code_record";
DELETE FROM "mes_wm_arrival_notice";
DELETE FROM "mes_wm_outsource_receipt";
DELETE FROM "mes_qc_iqc";
DELETE FROM "mes_qc_ipqc";
DELETE FROM "mes_pro_feedback";
DELETE FROM "mes_wm_product_produce";
DELETE FROM "mes_wm_product_produce_line";
DELETE FROM "mes_wm_product_produce_detail";
DELETE FROM "mes_wm_item_receipt";
DELETE FROM "mes_wm_item_receipt_line";
DELETE FROM "mes_wm_item_receipt_detail";
DELETE FROM "mes_wm_batch";
DELETE FROM "mes_wm_material_stock";
DELETE FROM "mes_pro_task";
DELETE FROM "mes_pro_route_process";
DELETE FROM "mes_qc_indicator_result";

View File

@@ -0,0 +1,832 @@
-- ----------------------------
-- MES 排班计划
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_cal_plan" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"code" varchar(64) DEFAULT NULL,
"name" varchar(255) DEFAULT NULL,
"calendar_type" tinyint DEFAULT NULL,
"start_date" timestamp DEFAULT NULL,
"end_date" timestamp DEFAULT NULL,
"shift_type" tinyint DEFAULT NULL,
"shift_method" tinyint DEFAULT NULL,
"shift_count" int DEFAULT NULL,
"status" tinyint DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 排班计划班次
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_cal_plan_shift" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"plan_id" bigint DEFAULT NULL,
"sort" int DEFAULT NULL,
"name" varchar(64) DEFAULT NULL,
"start_time" varchar(10) DEFAULT NULL,
"end_time" varchar(10) DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 排班计划班组
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_cal_plan_team" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"plan_id" bigint DEFAULT NULL,
"team_id" bigint DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 班组排班
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_cal_team_shift" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"plan_id" bigint DEFAULT NULL,
"team_id" bigint DEFAULT NULL,
"shift_id" bigint DEFAULT NULL,
"day" timestamp DEFAULT NULL,
"sort" int DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 编码规则表
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_md_auto_code_rule" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"code" varchar(64) NOT NULL,
"name" varchar(64) NOT NULL,
"description" varchar(255) DEFAULT NULL,
"max_length" int DEFAULT NULL,
"padded" bit NOT NULL DEFAULT FALSE,
"padded_char" varchar(1) DEFAULT NULL,
"padded_method" tinyint DEFAULT NULL,
"status" tinyint NOT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 编码规则组成表
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_md_auto_code_part" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"rule_id" bigint NOT NULL,
"sort" int NOT NULL,
"type" tinyint NOT NULL,
"length" int NOT NULL,
"date_format" varchar(20) DEFAULT NULL,
"fix_character" varchar(64) DEFAULT NULL,
"serial_start_no" int DEFAULT NULL,
"serial_step" int DEFAULT NULL,
"cycle_flag" bit DEFAULT FALSE,
"cycle_method" tinyint DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 编码生成记录表
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_md_auto_code_record" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"rule_id" bigint NOT NULL,
"result" varchar(64) DEFAULT NULL,
"serial_no" bigint DEFAULT NULL,
"input_char" varchar(64) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 到货通知单
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_wm_arrival_notice" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"code" varchar(64) DEFAULT NULL,
"name" varchar(255) DEFAULT NULL,
"purchase_order_code" varchar(64) DEFAULT NULL,
"vendor_id" bigint DEFAULT NULL,
"arrival_date" timestamp DEFAULT NULL,
"contact_name" varchar(64) DEFAULT NULL,
"contact_telephone" varchar(64) DEFAULT NULL,
"status" tinyint DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 外协入库单
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_wm_outsource_receipt" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"code" varchar(64) DEFAULT NULL,
"name" varchar(255) DEFAULT NULL,
"work_order_id" bigint DEFAULT NULL,
"vendor_id" bigint DEFAULT NULL,
"receipt_date" timestamp DEFAULT NULL,
"status" tinyint DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 来料检验单IQC
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_qc_iqc" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"code" varchar(64) DEFAULT NULL,
"name" varchar(255) DEFAULT NULL,
"template_id" bigint DEFAULT NULL,
"source_doc_type" int DEFAULT NULL,
"source_doc_id" bigint DEFAULT NULL,
"source_line_id" bigint DEFAULT NULL,
"source_doc_code" varchar(64) DEFAULT NULL,
"vendor_id" bigint DEFAULT NULL,
"vendor_batch" varchar(64) DEFAULT NULL,
"item_id" bigint DEFAULT NULL,
"received_quantity" decimal(24,6) DEFAULT NULL,
"check_quantity" decimal(24,6) DEFAULT NULL,
"qualified_quantity" decimal(24,6) DEFAULT NULL,
"unqualified_quantity" decimal(24,6) DEFAULT NULL,
"critical_rate" decimal(10,2) DEFAULT NULL,
"major_rate" decimal(10,2) DEFAULT NULL,
"minor_rate" decimal(10,2) DEFAULT NULL,
"critical_quantity" int DEFAULT NULL,
"major_quantity" int DEFAULT NULL,
"minor_quantity" int DEFAULT NULL,
"check_result" tinyint DEFAULT NULL,
"receive_date" timestamp DEFAULT NULL,
"inspect_date" timestamp DEFAULT NULL,
"inspector_user_id" bigint DEFAULT NULL,
"status" tinyint DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 过程检验单IPQC
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_qc_ipqc" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"code" varchar(64) DEFAULT NULL,
"name" varchar(500) DEFAULT NULL,
"type" tinyint DEFAULT NULL,
"template_id" bigint DEFAULT NULL,
"source_doc_type" int DEFAULT NULL,
"source_doc_id" bigint DEFAULT NULL,
"source_line_id" bigint DEFAULT NULL,
"source_doc_code" varchar(64) DEFAULT NULL,
"work_order_id" bigint DEFAULT NULL,
"task_id" bigint DEFAULT NULL,
"workstation_id" bigint DEFAULT NULL,
"process_id" bigint DEFAULT NULL,
"item_id" bigint DEFAULT NULL,
"check_quantity" decimal(14,2) DEFAULT NULL,
"qualified_quantity" decimal(14,2) DEFAULT NULL,
"unqualified_quantity" decimal(14,2) DEFAULT NULL,
"labor_scrap_quantity" decimal(14,2) DEFAULT NULL,
"material_scrap_quantity" decimal(14,2) DEFAULT NULL,
"other_scrap_quantity" decimal(14,2) DEFAULT NULL,
"critical_rate" decimal(14,2) DEFAULT NULL,
"major_rate" decimal(14,2) DEFAULT NULL,
"minor_rate" decimal(14,2) DEFAULT NULL,
"critical_quantity" int DEFAULT NULL,
"major_quantity" int DEFAULT NULL,
"minor_quantity" int DEFAULT NULL,
"check_result" tinyint DEFAULT NULL,
"inspect_date" timestamp DEFAULT NULL,
"inspector_user_id" bigint DEFAULT NULL,
"status" tinyint DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 生产报工
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_pro_feedback" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"code" varchar(64) DEFAULT NULL,
"type" tinyint DEFAULT NULL,
"channel" varchar(64) DEFAULT NULL,
"feedback_time" timestamp DEFAULT NULL,
"workstation_id" bigint DEFAULT NULL,
"route_id" bigint DEFAULT NULL,
"process_id" bigint DEFAULT NULL,
"work_order_id" bigint DEFAULT NULL,
"task_id" bigint DEFAULT NULL,
"item_id" bigint DEFAULT NULL,
"expire_date" timestamp DEFAULT NULL,
"lot_number" varchar(64) DEFAULT NULL,
"scheduled_quantity" decimal(14,2) DEFAULT NULL,
"feedback_quantity" decimal(14,2) DEFAULT NULL,
"qualified_quantity" decimal(14,2) DEFAULT NULL,
"unqualified_quantity" decimal(14,2) DEFAULT NULL,
"uncheck_quantity" decimal(14,2) DEFAULT NULL,
"labor_scrap_quantity" decimal(14,2) DEFAULT NULL,
"material_scrap_quantity" decimal(14,2) DEFAULT NULL,
"other_scrap_quantity" decimal(14,2) DEFAULT NULL,
"feedback_user_id" bigint DEFAULT NULL,
"approve_user_id" bigint DEFAULT NULL,
"status" tinyint DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 生产入库单主表
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_wm_product_produce" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"work_order_id" bigint DEFAULT NULL,
"feedback_id" bigint DEFAULT NULL,
"task_id" bigint DEFAULT NULL,
"workstation_id" bigint DEFAULT NULL,
"process_id" bigint DEFAULT NULL,
"produce_date" timestamp DEFAULT NULL,
"status" int DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 生产入库单行表
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_wm_product_produce_line" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"produce_id" bigint DEFAULT NULL,
"feedback_id" bigint DEFAULT NULL,
"item_id" bigint DEFAULT NULL,
"quantity" decimal(12,2) DEFAULT NULL,
"batch_id" bigint DEFAULT NULL,
"batch_code" varchar(255) DEFAULT NULL,
"expire_date" timestamp DEFAULT NULL,
"lot_number" varchar(128) DEFAULT NULL,
"quality_status" int DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 生产入库明细表
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_wm_product_produce_detail" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"produce_id" bigint DEFAULT NULL,
"line_id" bigint DEFAULT NULL,
"item_id" bigint DEFAULT NULL,
"quantity" decimal(12,2) DEFAULT NULL,
"batch_id" bigint DEFAULT NULL,
"batch_code" varchar(255) DEFAULT NULL,
"warehouse_id" bigint DEFAULT NULL,
"location_id" bigint DEFAULT NULL,
"area_id" bigint DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 出货检验单OQC
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_qc_oqc" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"code" varchar(64) NOT NULL,
"name" varchar(500) NOT NULL,
"template_id" bigint NOT NULL,
"source_doc_type" int DEFAULT NULL,
"source_doc_id" bigint DEFAULT NULL,
"source_line_id" bigint DEFAULT NULL,
"source_doc_code" varchar(64) DEFAULT NULL,
"client_id" bigint NOT NULL,
"batch_code" varchar(64) DEFAULT NULL,
"item_id" bigint NOT NULL,
"min_check_quantity" int DEFAULT 1,
"max_unqualified_quantity" int DEFAULT 0,
"out_quantity" decimal(14,2) NOT NULL,
"check_quantity" decimal(14,2) DEFAULT NULL,
"qualified_quantity" decimal(14,2) DEFAULT 0.00,
"unqualified_quantity" decimal(14,2) DEFAULT 0.00,
"critical_rate" decimal(14,2) DEFAULT 0.00,
"major_rate" decimal(14,2) DEFAULT 0.00,
"minor_rate" decimal(14,2) DEFAULT 0.00,
"critical_quantity" int DEFAULT 0,
"major_quantity" int DEFAULT 0,
"minor_quantity" int DEFAULT 0,
"check_result" tinyint DEFAULT NULL,
"out_date" timestamp DEFAULT NULL,
"inspect_date" timestamp DEFAULT NULL,
"inspector_user_id" bigint DEFAULT NULL,
"status" tinyint NOT NULL DEFAULT 0,
"remark" varchar(500) DEFAULT '',
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 退货检验单RQC
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_qc_rqc" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"code" varchar(64) NOT NULL,
"name" varchar(500) NOT NULL,
"template_id" bigint NOT NULL,
"source_doc_type" int DEFAULT NULL,
"source_doc_id" bigint DEFAULT NULL,
"source_line_id" bigint DEFAULT NULL,
"source_doc_code" varchar(64) DEFAULT NULL,
"type" int DEFAULT NULL,
"item_id" bigint NOT NULL,
"batch_code" varchar(128) DEFAULT NULL,
"check_quantity" decimal(14,2) DEFAULT NULL,
"qualified_quantity" decimal(14,2) DEFAULT 0.00,
"unqualified_quantity" decimal(14,2) DEFAULT 0.00,
"critical_rate" decimal(14,2) DEFAULT 0.00,
"major_rate" decimal(14,2) DEFAULT 0.00,
"minor_rate" decimal(14,2) DEFAULT 0.00,
"critical_quantity" int DEFAULT 0,
"major_quantity" int DEFAULT 0,
"minor_quantity" int DEFAULT 0,
"check_result" tinyint DEFAULT NULL,
"inspect_date" timestamp DEFAULT NULL,
"inspector_user_id" bigint DEFAULT NULL,
"status" tinyint NOT NULL DEFAULT 0,
"remark" varchar(500) DEFAULT '',
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 销售出库单行
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_wm_product_sales_line" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"sales_id" bigint NOT NULL,
"notice_line_id" bigint DEFAULT NULL,
"item_id" bigint NOT NULL,
"quantity" decimal(20,6) NOT NULL,
"batch_id" bigint DEFAULT NULL,
"batch_code" varchar(255) DEFAULT NULL,
"material_stock_id" bigint DEFAULT NULL,
"oqc_check_flag" tinyint DEFAULT NULL,
"oqc_id" bigint DEFAULT NULL,
"quality_status" int DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 生产退料单行
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_wm_return_issue_line" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"issue_id" bigint NOT NULL,
"material_stock_id" bigint DEFAULT NULL,
"item_id" bigint NOT NULL,
"quantity" decimal(12,2) NOT NULL DEFAULT 0.00,
"batch_id" bigint DEFAULT NULL,
"batch_code" varchar(255) DEFAULT NULL,
"rqc_id" bigint DEFAULT NULL,
"rqc_check_flag" bit NOT NULL DEFAULT FALSE,
"quality_status" int DEFAULT NULL,
"remark" varchar(500) DEFAULT '',
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 销售退货单行
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_wm_return_sales_line" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"return_id" bigint NOT NULL,
"item_id" bigint NOT NULL,
"quantity" decimal(12,2) NOT NULL DEFAULT 0.00,
"batch_id" bigint DEFAULT NULL,
"batch_code" varchar(255) DEFAULT NULL,
"rqc_id" bigint DEFAULT NULL,
"rqc_check_flag" bit NOT NULL DEFAULT FALSE,
"quality_status" int DEFAULT NULL,
"remark" varchar(500) DEFAULT '',
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 采购入库单
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_wm_item_receipt" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"code" varchar(64) NOT NULL,
"name" varchar(255) DEFAULT NULL,
"iqc_id" bigint DEFAULT NULL,
"notice_id" bigint DEFAULT NULL,
"vendor_id" bigint DEFAULT NULL,
"purchase_order_code" varchar(64) DEFAULT NULL,
"receipt_date" timestamp DEFAULT NULL,
"status" tinyint NOT NULL DEFAULT 0,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 采购入库单行
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_wm_item_receipt_line" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"receipt_id" bigint NOT NULL,
"arrival_notice_line_id" bigint DEFAULT NULL,
"item_id" bigint NOT NULL,
"received_quantity" decimal(14,2) DEFAULT NULL,
"batch_id" bigint DEFAULT NULL,
"batch_code" varchar(255) DEFAULT NULL,
"production_date" timestamp DEFAULT NULL,
"expire_date" timestamp DEFAULT NULL,
"lot_number" varchar(128) DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 采购入库明细
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_wm_item_receipt_detail" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"line_id" bigint NOT NULL,
"receipt_id" bigint NOT NULL,
"item_id" bigint NOT NULL,
"quantity" decimal(14,2) DEFAULT NULL,
"batch_id" bigint DEFAULT NULL,
"warehouse_id" bigint DEFAULT NULL,
"location_id" bigint DEFAULT NULL,
"area_id" bigint DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 物料消耗记录表
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_wm_item_consume" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"work_order_id" bigint NOT NULL,
"task_id" bigint NOT NULL,
"workstation_id" bigint NOT NULL,
"process_id" bigint NOT NULL,
"feedback_id" bigint NOT NULL,
"consume_date" timestamp NOT NULL,
"status" tinyint NOT NULL DEFAULT 0,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 物料消耗记录行表
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_wm_item_consume_line" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"consume_id" bigint NOT NULL,
"item_id" bigint NOT NULL,
"quantity" decimal(14,2) NOT NULL,
"batch_id" bigint DEFAULT NULL,
"batch_code" varchar(128) DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 物料消耗记录明细表
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_wm_item_consume_detail" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"consume_id" bigint NOT NULL,
"line_id" bigint NOT NULL,
"material_stock_id" bigint DEFAULT NULL,
"item_id" bigint NOT NULL,
"quantity" decimal(14,2) NOT NULL,
"batch_id" bigint DEFAULT NULL,
"batch_code" varchar(128) DEFAULT NULL,
"warehouse_id" bigint NOT NULL,
"location_id" bigint NOT NULL,
"area_id" bigint NOT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 批次记录表
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_wm_batch" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"code" varchar(128) DEFAULT NULL,
"item_id" bigint DEFAULT NULL,
"produce_date" timestamp DEFAULT NULL,
"expire_date" timestamp DEFAULT NULL,
"receipt_date" timestamp DEFAULT NULL,
"vendor_id" bigint DEFAULT NULL,
"client_id" bigint DEFAULT NULL,
"sales_order_code" varchar(64) DEFAULT NULL,
"purchase_order_code" varchar(64) DEFAULT NULL,
"work_order_id" bigint DEFAULT NULL,
"task_id" bigint DEFAULT NULL,
"workstation_id" bigint DEFAULT NULL,
"tool_id" bigint DEFAULT NULL,
"mold_id" bigint DEFAULT NULL,
"lot_number" varchar(128) DEFAULT NULL,
"quality_status" int DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 库存台账
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_wm_material_stock" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"item_type_id" bigint DEFAULT NULL,
"item_id" bigint DEFAULT NULL,
"batch_id" bigint DEFAULT NULL,
"batch_code" varchar(255) DEFAULT NULL,
"warehouse_id" bigint DEFAULT NULL,
"location_id" bigint DEFAULT NULL,
"area_id" bigint DEFAULT NULL,
"vendor_id" bigint DEFAULT NULL,
"quantity" decimal(14,2) DEFAULT NULL,
"receipt_time" timestamp DEFAULT NULL,
"frozen" bit DEFAULT FALSE,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 生产任务
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_pro_task" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"code" varchar(64) DEFAULT NULL,
"name" varchar(255) DEFAULT NULL,
"work_order_id" bigint DEFAULT NULL,
"workstation_id" bigint DEFAULT NULL,
"route_id" bigint DEFAULT NULL,
"process_id" bigint DEFAULT NULL,
"item_id" bigint DEFAULT NULL,
"quantity" decimal(14,2) DEFAULT NULL,
"produced_quantity" decimal(14,2) DEFAULT NULL,
"qualify_quantity" decimal(14,2) DEFAULT NULL,
"unqualify_quantity" decimal(14,2) DEFAULT NULL,
"changed_quantity" decimal(14,2) DEFAULT NULL,
"client_id" bigint DEFAULT NULL,
"start_time" timestamp DEFAULT NULL,
"duration" int DEFAULT NULL,
"end_time" timestamp DEFAULT NULL,
"color_code" varchar(20) DEFAULT NULL,
"finish_date" timestamp DEFAULT NULL,
"cancel_date" timestamp DEFAULT NULL,
"status" int DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 工艺路线工序
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_pro_route_process" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"route_id" bigint DEFAULT NULL,
"process_id" bigint DEFAULT NULL,
"sort" int DEFAULT NULL,
"next_process_id" bigint DEFAULT NULL,
"link_type" int DEFAULT NULL,
"prepare_time" int DEFAULT NULL,
"wait_time" int DEFAULT NULL,
"color_code" varchar(20) DEFAULT NULL,
"key_flag" bit DEFAULT FALSE,
"check_flag" bit DEFAULT FALSE,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 销售出库明细
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_wm_product_sales_detail" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"line_id" bigint NOT NULL,
"sales_id" bigint NOT NULL,
"item_id" bigint NOT NULL,
"quantity" decimal(14,2) DEFAULT NULL,
"material_stock_id" bigint DEFAULT NULL,
"batch_id" bigint DEFAULT NULL,
"batch_code" varchar(255) DEFAULT NULL,
"warehouse_id" bigint DEFAULT NULL,
"location_id" bigint DEFAULT NULL,
"area_id" bigint DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);
-- ----------------------------
-- MES 检验结果记录
-- ----------------------------
CREATE TABLE IF NOT EXISTS "mes_qc_indicator_result" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"code" varchar(64) DEFAULT NULL,
"qc_id" bigint DEFAULT NULL,
"qc_type" int DEFAULT NULL,
"item_id" bigint DEFAULT NULL,
"sn" varchar(128) DEFAULT NULL,
"remark" varchar(500) DEFAULT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);