yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImplTest.java
@@ -1,17 +1,35 @@
package cn.iocoder.yudao.module.mes.service.pro.feedback;
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.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemBatchConfigApi;
import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi;
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackApproveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackQuickCreateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackQuickCreateRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackSubmitReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
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.dataobject.pro.workorder.MesProWorkOrderProcessDO;
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.pro.MesProTaskStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
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.MesProWorkOrderBomService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderParamRecordService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
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;
@@ -22,6 +40,8 @@
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
@@ -31,7 +51,7 @@
/**
 * {@link MesProFeedbackServiceImpl} 的单元测试
 *
 * @author 芋道源码
 * @author 超级管理员
 */
@Import(MesProFeedbackServiceImpl.class)
public class MesProFeedbackServiceImplTest extends BaseDbUnitTest {
@@ -56,6 +76,24 @@
    private MesWmProductProduceService productProduceService;
    @MockitoBean
    private MesWmProductProduceLineService produceLineService;
    @MockitoBean
    private MesProWorkOrderParamRecordService paramRecordService;
    @MockitoBean
    private MesProWorkOrderBomService workOrderBomService;
    @MockitoBean
    private MesProWorkOrderProcessService workOrderProcessService;
    @MockitoBean
    private MesProProcessService processService;
    @MockitoBean
    private MesProMpsApi mpsApi;
    @MockitoBean
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @MockitoBean
    private MesMdmItemService mdmItemService;
    @MockitoBean
    private MdmItemBatchConfigApi itemBatchConfigApi;
    @MockitoBean
    private BpmProcessInstanceApi processInstanceApi;
    @Test
    public void testUpdateProFeedbackWhenIpqcFinish_success_withUnqualified() {
@@ -222,7 +260,7 @@
        // mock: 产品产出单
        MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class);
        when(productProduceService.generateProductProduce(any(), eq(false))).thenReturn(produce);
        when(productProduceService.generateProductProduce(any(), eq(false), any())).thenReturn(produce);
        // mock: 产出行(用于 updateTaskAndWorkOrderByFeedback)
        MesWmProductProduceLineDO qualifiedLine = MesWmProductProduceLineDO.builder()
@@ -232,7 +270,9 @@
                .thenReturn(ListUtil.of(qualifiedLine));
        // 调用
        boolean result = feedbackService.approveFeedback(feedback.getId());
        MesProFeedbackApproveReqVO reqVO = new MesProFeedbackApproveReqVO();
        reqVO.setId(feedback.getId());
        boolean result = feedbackService.approveFeedback(reqVO);
        // 断言 1:返回 true(已完成)
        assertTrue(result);
@@ -243,7 +283,7 @@
        assertEquals(0, BigDecimal.ZERO.compareTo(updated.getUncheckQuantity()));
        // 断言 3:调用了产出单生成 + 入库 + 任务/工单更新
        verify(productProduceService).generateProductProduce(any(), eq(false));
        verify(productProduceService).generateProductProduce(any(), eq(false), any());
        verify(productProduceService).finishProductProduce(produce.getId());
        verify(taskService).updateProducedQuantity(eq(feedback.getTaskId()),
                any(BigDecimal.class), any(BigDecimal.class), any(BigDecimal.class));
@@ -273,7 +313,9 @@
                .thenReturn(routeProcess);
        // 调用
        boolean result = feedbackService.approveFeedback(feedback.getId());
        MesProFeedbackApproveReqVO reqVO = new MesProFeedbackApproveReqVO();
        reqVO.setId(feedback.getId());
        boolean result = feedbackService.approveFeedback(reqVO);
        // 断言 1:返回 false(待检验)
        assertFalse(result);
@@ -283,7 +325,7 @@
        assertEquals(MesProFeedbackStatusEnum.UNCHECK.getStatus(), updated.getStatus());
        // 断言 3:生成了待检产出单,但没有 finishProductProduce
        verify(productProduceService).generateProductProduce(any(), eq(true));
        verify(productProduceService).generateProductProduce(any(), eq(true), any());
        verify(productProduceService, never()).finishProductProduce(anyLong());
        // 断言 4:没有更新任务/工单数量(等 IPQC 回调)
@@ -313,7 +355,9 @@
                .thenReturn(routeProcess);
        // 调用
        boolean result = feedbackService.approveFeedback(feedback.getId());
        MesProFeedbackApproveReqVO reqVO = new MesProFeedbackApproveReqVO();
        reqVO.setId(feedback.getId());
        boolean result = feedbackService.approveFeedback(reqVO);
        // 断言 1:返回 true(已完成)
        assertTrue(result);
@@ -323,7 +367,7 @@
        assertEquals(MesProFeedbackStatusEnum.FINISHED.getStatus(), updated.getStatus());
        // 断言 3:不生成产出单,不更新任务/工单
        verify(productProduceService, never()).generateProductProduce(any(), anyBoolean());
        verify(productProduceService, never()).generateProductProduce(any(), anyBoolean(), any());
        verify(taskService, never()).updateProducedQuantity(anyLong(), any(), any(), any());
    }
@@ -349,11 +393,13 @@
                .thenReturn(routeProcess);
        // 调用,应该抛异常
        MesProFeedbackApproveReqVO reqVO = new MesProFeedbackApproveReqVO();
        reqVO.setId(feedback.getId());
        assertThrows(Exception.class, () ->
                feedbackService.approveFeedback(feedback.getId()));
                feedbackService.approveFeedback(reqVO));
        // 断言:不应该执行任何后续操作
        verify(productProduceService, never()).generateProductProduce(any(), anyBoolean());
        verify(productProduceService, never()).generateProductProduce(any(), anyBoolean(), any());
        verify(itemConsumeService, never()).generateItemConsume(any());
    }
@@ -380,7 +426,9 @@
                .thenReturn(routeProcess);
        // 调用
        boolean result = feedbackService.approveFeedback(feedback.getId());
        MesProFeedbackApproveReqVO reqVO = new MesProFeedbackApproveReqVO();
        reqVO.setId(feedback.getId());
        boolean result = feedbackService.approveFeedback(reqVO);
        // 断言 1:返回 true(直接完成,不走 UNCHECK)
        assertTrue(result);
@@ -391,7 +439,277 @@
        assertEquals(0, BigDecimal.ZERO.compareTo(updated.getUncheckQuantity()));
        // 断言 3:非关键工序不生成产出单
        verify(productProduceService, never()).generateProductProduce(any(), anyBoolean());
        verify(productProduceService, never()).generateProductProduce(any(), anyBoolean(), any());
    }
    // ==================== 顺序报工(上一道工序完成后才能报下一道)测试 ====================
    /**
     * 构造"上道工序(prev)+ 当前工序(cur)"的工单工序链(故意乱序传入,验证按 sort 排序后取前驱)
     */
    private List<MesProWorkOrderProcessDO> buildTwoProcessChain(Long workOrderId, Long prevProcessId,
                                                                Long curProcessId) {
        return ListUtil.of(
                MesProWorkOrderProcessDO.builder().workOrderId(workOrderId).processId(curProcessId).sort(2).build(),
                MesProWorkOrderProcessDO.builder().workOrderId(workOrderId).processId(prevProcessId).sort(1).build());
    }
    @Test
    public void testQuickCreateFeedback_blockedWhenPreviousProcessNotFinished() {
        // 准备:当前任务在第 2 道工序,上道工序任务只做了 60/100
        Long workOrderId = randomLongId();
        Long routeId = randomLongId();
        Long prevProcessId = randomLongId();
        Long curProcessId = randomLongId();
        Long curTaskId = randomLongId();
        MesProTaskDO curTask = randomPojo(MesProTaskDO.class, o -> {
            o.setId(curTaskId);
            o.setWorkOrderId(workOrderId);
            o.setRouteId(routeId);
            o.setProcessId(curProcessId);
            o.setQuantity(BigDecimal.valueOf(100));
            o.setProducedQuantity(BigDecimal.ZERO);
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.validateTaskNotFinished(curTaskId)).thenReturn(curTask);
        when(workOrderProcessService.getWorkOrderProcessListByWorkOrderId(workOrderId))
                .thenReturn(buildTwoProcessChain(workOrderId, prevProcessId, curProcessId));
        MesProTaskDO prevTask = randomPojo(MesProTaskDO.class, o -> {
            o.setWorkOrderId(workOrderId);
            o.setProcessId(prevProcessId);
            o.setQuantity(BigDecimal.valueOf(100));
            o.setProducedQuantity(BigDecimal.valueOf(60));
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.getTaskListByWorkOrderIds(Collections.singletonList(workOrderId)))
                .thenReturn(List.of(prevTask));
        when(processService.getProcess(prevProcessId)).thenReturn(
                randomPojo(MesProProcessDO.class, o -> o.setName("CNC加工")));
        // 调用:应被拦截
        MesProFeedbackQuickCreateReqVO req = new MesProFeedbackQuickCreateReqVO();
        req.setTaskId(curTaskId);
        req.setFeedbackQuantity(BigDecimal.TEN);
        ServiceException ex = assertThrows(ServiceException.class, () -> feedbackService.quickCreateFeedback(req));
        assertTrue(ex.getMessage().contains("尚未报工完成"));
        assertTrue(ex.getMessage().contains("CNC加工"));
        // 未真正生成报工单
        verify(autoCodeRecordService, never()).generateAutoCode(anyString());
    }
    @Test
    public void testQuickCreateFeedback_blockedWhenPreviousMultiTaskAnyNotFinished() {
        // 准备:上道工序有 2 个任务,一个做满、一个未做满,仍应拦截
        Long workOrderId = randomLongId();
        Long routeId = randomLongId();
        Long prevProcessId = randomLongId();
        Long curProcessId = randomLongId();
        Long curTaskId = randomLongId();
        MesProTaskDO curTask = randomPojo(MesProTaskDO.class, o -> {
            o.setId(curTaskId);
            o.setWorkOrderId(workOrderId);
            o.setRouteId(routeId);
            o.setProcessId(curProcessId);
            o.setQuantity(BigDecimal.valueOf(100));
            o.setProducedQuantity(BigDecimal.ZERO);
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.validateTaskNotFinished(curTaskId)).thenReturn(curTask);
        when(workOrderProcessService.getWorkOrderProcessListByWorkOrderId(workOrderId))
                .thenReturn(buildTwoProcessChain(workOrderId, prevProcessId, curProcessId));
        MesProTaskDO finishedTask = randomPojo(MesProTaskDO.class, o -> {
            o.setWorkOrderId(workOrderId);
            o.setProcessId(prevProcessId);
            o.setQuantity(BigDecimal.valueOf(50));
            o.setProducedQuantity(BigDecimal.valueOf(50)); // 已做满
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        MesProTaskDO partialTask = randomPojo(MesProTaskDO.class, o -> {
            o.setWorkOrderId(workOrderId);
            o.setProcessId(prevProcessId);
            o.setQuantity(BigDecimal.valueOf(50));
            o.setProducedQuantity(BigDecimal.valueOf(20)); // 未做满
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.getTaskListByWorkOrderIds(Collections.singletonList(workOrderId)))
                .thenReturn(ListUtil.of(finishedTask, partialTask));
        MesProFeedbackQuickCreateReqVO req = new MesProFeedbackQuickCreateReqVO();
        req.setTaskId(curTaskId);
        req.setFeedbackQuantity(BigDecimal.TEN);
        ServiceException ex = assertThrows(ServiceException.class, () -> feedbackService.quickCreateFeedback(req));
        assertTrue(ex.getMessage().contains("尚未报工完成"));
    }
    @Test
    public void testQuickCreateFeedback_allowedWhenPreviousProcessFinished() {
        // 准备:上道工序任务已做满(100/100),当前为第 2 道工序,应放行
        Long workOrderId = randomLongId();
        Long routeId = randomLongId();
        Long prevProcessId = randomLongId();
        Long curProcessId = randomLongId();
        Long curTaskId = randomLongId();
        MesProTaskDO curTask = randomPojo(MesProTaskDO.class, o -> {
            o.setId(curTaskId);
            o.setWorkOrderId(workOrderId);
            o.setRouteId(routeId);
            o.setProcessId(curProcessId);
            o.setQuantity(BigDecimal.valueOf(100));
            o.setProducedQuantity(BigDecimal.ZERO);
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.validateTaskNotFinished(curTaskId)).thenReturn(curTask);
        when(workOrderProcessService.getWorkOrderProcessListByWorkOrderId(workOrderId))
                .thenReturn(buildTwoProcessChain(workOrderId, prevProcessId, curProcessId));
        MesProTaskDO prevTask = randomPojo(MesProTaskDO.class, o -> {
            o.setWorkOrderId(workOrderId);
            o.setProcessId(prevProcessId);
            o.setQuantity(BigDecimal.valueOf(100));
            o.setProducedQuantity(BigDecimal.valueOf(100)); // 已做满
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.getTaskListByWorkOrderIds(Collections.singletonList(workOrderId)))
                .thenReturn(List.of(prevTask));
        // 走通后续逻辑的桩
        when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, curProcessId))
                .thenReturn(MesProRouteProcessDO.builder().routeId(routeId).processId(curProcessId)
                        .keyFlag(false).checkFlag(false).build());
        when(autoCodeRecordService.generateAutoCode(anyString())).thenReturn("FB-ORDER-1");
        // 调用:应成功生成报工单
        MesProFeedbackQuickCreateReqVO req = new MesProFeedbackQuickCreateReqVO();
        req.setTaskId(curTaskId);
        req.setFeedbackQuantity(BigDecimal.TEN);
        MesProFeedbackQuickCreateRespVO resp = feedbackService.quickCreateFeedback(req);
        assertNotNull(resp);
        assertNotNull(resp.getFeedbackId());
        verify(autoCodeRecordService).generateAutoCode(anyString());
    }
    @Test
    public void testQuickCreateFeedback_allowedWhenPreviousProcessAllCancelled() {
        // 准备:上道工序任务全部取消,视为无前置要求,当前第 2 道工序应放行
        Long workOrderId = randomLongId();
        Long routeId = randomLongId();
        Long prevProcessId = randomLongId();
        Long curProcessId = randomLongId();
        Long curTaskId = randomLongId();
        MesProTaskDO curTask = randomPojo(MesProTaskDO.class, o -> {
            o.setId(curTaskId);
            o.setWorkOrderId(workOrderId);
            o.setRouteId(routeId);
            o.setProcessId(curProcessId);
            o.setQuantity(BigDecimal.valueOf(100));
            o.setProducedQuantity(BigDecimal.ZERO);
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.validateTaskNotFinished(curTaskId)).thenReturn(curTask);
        when(workOrderProcessService.getWorkOrderProcessListByWorkOrderId(workOrderId))
                .thenReturn(buildTwoProcessChain(workOrderId, prevProcessId, curProcessId));
        MesProTaskDO cancelledTask = randomPojo(MesProTaskDO.class, o -> {
            o.setWorkOrderId(workOrderId);
            o.setProcessId(prevProcessId);
            o.setQuantity(BigDecimal.valueOf(100));
            o.setProducedQuantity(BigDecimal.ZERO);
            o.setStatus(MesProTaskStatusEnum.CANCELED.getStatus());
        });
        when(taskService.getTaskListByWorkOrderIds(Collections.singletonList(workOrderId)))
                .thenReturn(List.of(cancelledTask));
        when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, curProcessId))
                .thenReturn(MesProRouteProcessDO.builder().routeId(routeId).processId(curProcessId)
                        .keyFlag(false).checkFlag(false).build());
        when(autoCodeRecordService.generateAutoCode(anyString())).thenReturn("FB-ORDER-2");
        MesProFeedbackQuickCreateReqVO req = new MesProFeedbackQuickCreateReqVO();
        req.setTaskId(curTaskId);
        req.setFeedbackQuantity(BigDecimal.TEN);
        MesProFeedbackQuickCreateRespVO resp = feedbackService.quickCreateFeedback(req);
        assertNotNull(resp);
        assertNotNull(resp.getFeedbackId());
    }
    @Test
    public void testQuickCreateFeedback_allowedWhenCurrentIsFirstProcess() {
        // 准备:当前任务就是第 1 道工序(链中只有一道工序),无前置,应放行
        Long workOrderId = randomLongId();
        Long routeId = randomLongId();
        Long curProcessId = randomLongId();
        Long curTaskId = randomLongId();
        MesProTaskDO curTask = randomPojo(MesProTaskDO.class, o -> {
            o.setId(curTaskId);
            o.setWorkOrderId(workOrderId);
            o.setRouteId(routeId);
            o.setProcessId(curProcessId);
            o.setQuantity(BigDecimal.valueOf(100));
            o.setProducedQuantity(BigDecimal.ZERO);
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.validateTaskNotFinished(curTaskId)).thenReturn(curTask);
        when(workOrderProcessService.getWorkOrderProcessListByWorkOrderId(workOrderId))
                .thenReturn(ListUtil.of(MesProWorkOrderProcessDO.builder()
                        .workOrderId(workOrderId).processId(curProcessId).sort(1).build()));
        when(routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, curProcessId))
                .thenReturn(MesProRouteProcessDO.builder().routeId(routeId).processId(curProcessId)
                        .keyFlag(false).checkFlag(false).build());
        when(autoCodeRecordService.generateAutoCode(anyString())).thenReturn("FB-ORDER-3");
        MesProFeedbackQuickCreateReqVO req = new MesProFeedbackQuickCreateReqVO();
        req.setTaskId(curTaskId);
        req.setFeedbackQuantity(BigDecimal.TEN);
        MesProFeedbackQuickCreateRespVO resp = feedbackService.quickCreateFeedback(req);
        assertNotNull(resp);
        assertNotNull(resp.getFeedbackId());
    }
    @Test
    public void testSubmitFeedback_blockedWhenPreviousProcessNotFinished() {
        // 准备:已存在草稿报工单(第 2 道工序),上道工序未做满,提交应被拦截
        Long workOrderId = randomLongId();
        Long routeId = randomLongId();
        Long prevProcessId = randomLongId();
        Long curProcessId = randomLongId();
        Long curTaskId = randomLongId();
        MesProFeedbackDO feedback = randomPojo(MesProFeedbackDO.class, o -> {
            o.setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus());
            o.setTaskId(curTaskId);
            o.setWorkOrderId(workOrderId);
            o.setRouteId(routeId);
            o.setProcessId(curProcessId);
        });
        feedbackMapper.insert(feedback);
        MesProTaskDO curTask = randomPojo(MesProTaskDO.class, o -> {
            o.setId(curTaskId);
            o.setWorkOrderId(workOrderId);
            o.setRouteId(routeId);
            o.setProcessId(curProcessId);
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.getTask(curTaskId)).thenReturn(curTask);
        when(workOrderProcessService.getWorkOrderProcessListByWorkOrderId(workOrderId))
                .thenReturn(buildTwoProcessChain(workOrderId, prevProcessId, curProcessId));
        MesProTaskDO prevTask = randomPojo(MesProTaskDO.class, o -> {
            o.setWorkOrderId(workOrderId);
            o.setProcessId(prevProcessId);
            o.setQuantity(BigDecimal.valueOf(100));
            o.setProducedQuantity(BigDecimal.valueOf(30));
            o.setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        });
        when(taskService.getTaskListByWorkOrderIds(Collections.singletonList(workOrderId)))
                .thenReturn(List.of(prevTask));
        MesProFeedbackSubmitReqVO req = new MesProFeedbackSubmitReqVO();
        req.setId(feedback.getId());
        ServiceException ex = assertThrows(ServiceException.class, () -> feedbackService.submitFeedback(req));
        assertTrue(ex.getMessage().contains("尚未报工完成"));
    }
}