diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/MesProFeedbackController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/MesProFeedbackController.java index f601f05..787c3bd 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/MesProFeedbackController.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/MesProFeedbackController.java @@ -49,7 +49,7 @@ import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPOR import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; -import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + @Tag(name = "管理后台 - MES 生产报工") @RestController @@ -158,7 +158,7 @@ public class MesProFeedbackController { @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('mes:pro-feedback:approve')") public CommonResult approveFeedback(@RequestParam("id") Long id) { - return success(feedbackService.approveFeedback(id, getLoginUserId())); + return success(feedbackService.approveFeedback(id)); } // ==================== 拼接 VO ==================== diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackService.java index 0dfd9fd..b528c35 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackService.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackService.java @@ -91,11 +91,10 @@ public interface MesProFeedbackService { * * * - * @param id 报工单编号 - * @param userId 当前操作用户编号(审核人) + * @param id 报工单编号 * @return true=已完成, false=待检验(需等质检回调) */ - boolean approveFeedback(Long id, Long userId); + boolean approveFeedback(Long id); /** * IPQC 完成后回调:完成报工单并更新任务/工单进度 diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java index 4cf0f9a..e7148ee 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java @@ -31,11 +31,9 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; -import java.time.LocalDateTime; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; /** @@ -116,11 +114,9 @@ public class MesProFeedbackServiceImpl implements MesProFeedbackService { // 1. 校验存在 + 草稿状态 validateFeedbackStatusPrepare(id); - // 2. 更新状态为审批中,记录报工人和报工时间 + // 2. 更新状态为审批中(报工人和报工时间由表单保存时确定,提交不覆盖) feedbackMapper.updateById(new MesProFeedbackDO().setId(id) - .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus()) - .setFeedbackUserId(getLoginUserId()) - .setFeedbackTime(LocalDateTime.now())); + .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus())); } @Override @@ -135,34 +131,35 @@ public class MesProFeedbackServiceImpl implements MesProFeedbackService { @Override @Transactional(rollbackFor = Exception.class) - public boolean approveFeedback(Long id, Long userId) { - // 1.1.a 校验存在 + 审批中状态 + public boolean approveFeedback(Long id) { + // 1.1 校验存在 + 审批中状态 MesProFeedbackDO feedback = validateFeedbackStatusApproving(id); - // 1.1.b 校验报工数量 > 0 + // 1.2 校验报工数量 > 0 if (feedback.getFeedbackQuantity() == null || feedback.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) { throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE); } - // 1.2.a 校验任务未完成 + // 1.3 校验任务未完成 taskService.validateTaskNotFinished(feedback.getTaskId()); - // 1.2.b 仍有待检数量时不能执行 - if (feedback.getUncheckQuantity() != null + + // 2.1 查询工序的关键工序标识 + 检验标识(需在 uncheckQuantity 校验之前,因为质检工序允许 uncheckQuantity > 0) + MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId( + feedback.getRouteId(), feedback.getProcessId()); + boolean keyFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getKeyFlag()); + boolean checkFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getCheckFlag()); + // 2.2 非质检工序:仍有待检数量时不能审批(质检工序的 uncheckQuantity > 0 是正常状态,不做拦截) + if (!checkFlag + && feedback.getUncheckQuantity() != null && feedback.getUncheckQuantity().compareTo(BigDecimal.ZERO) > 0) { throw exception(PRO_FEEDBACK_UNCHECK_QUANTITY_EXISTS, feedback.getUncheckQuantity()); } - // 2. 物料消耗:根据工序 BOM 生成消耗记录并执行扣减 + // 3. 物料消耗:根据工序 BOM 生成消耗记录并执行扣减 MesWmItemConsumeDO itemConsume = itemConsumeService.generateItemConsume(feedback); if (itemConsume != null) { itemConsumeService.finishItemConsume(itemConsume.getId()); } - // 3. 查询工序的关键工序标识 + 检验标识 - MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId( - feedback.getRouteId(), feedback.getProcessId()); - boolean keyFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getKeyFlag()); - boolean checkFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getCheckFlag()); - // 4. 关键工序:生成产出单,并根据是否需要检验决定入库方式 if (keyFlag) { // 4.1 需要检验:生成产出单(质量状态=待检验),更新报工状态为待检验,等质检完成回调后再入库 @@ -179,10 +176,10 @@ public class MesProFeedbackServiceImpl implements MesProFeedbackService { updateTaskAndWorkOrderByFeedback(feedback); } - // 5. 非关键工序:不生成产出单,不更新任务/工单数量,直接完成 + // 5. 非关键工序 / 关键非质检工序:直接完成(清零 uncheckQuantity 防止 !key+check 留脏数据) feedbackMapper.updateById(new MesProFeedbackDO().setId(id) .setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus()) - .setApproveUserId(userId)); + .setUncheckQuantity(BigDecimal.ZERO)); return true; // 已完成 } diff --git a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImplTest.java b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImplTest.java index d379b75..c7e74c5 100644 --- a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImplTest.java +++ b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImplTest.java @@ -3,6 +3,8 @@ 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; @@ -23,8 +25,7 @@ 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.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; /** @@ -194,4 +195,203 @@ public class MesProFeedbackServiceImplTest extends BaseDbUnitTest { 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+check),uncheckQuantity > 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()); + } + } diff --git a/yudao-module-mes/src/test/resources/sql/create_tables.sql b/yudao-module-mes/src/test/resources/sql/create_tables.sql index 77922be..91795c4 100644 --- a/yudao-module-mes/src/test/resources/sql/create_tables.sql +++ b/yudao-module-mes/src/test/resources/sql/create_tables.sql @@ -705,17 +705,17 @@ CREATE TABLE IF NOT EXISTS "mes_wm_batch" ( -- ---------------------------- 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, + "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 '',