9 小时以前 74ad846852f42b4e2258bfddd5c2a33cc1ce9847
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -12,6 +12,7 @@
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
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.MesProWorkOrderBomDO;
@@ -24,6 +25,7 @@
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.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;
@@ -33,6 +35,7 @@
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 cn.iocoder.yudao.module.mes.service.wm.productreceipt.MesWmProductReceiptService;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemBatchConfigApi;
@@ -48,6 +51,7 @@
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
@@ -78,6 +82,8 @@
    @Resource
    private MesProRouteProcessService routeProcessService;
    @Resource
    private MesProProcessService processService;
    @Resource
    private MesProWorkOrderBomService workOrderBomService;
    @Resource
    private MesProWorkOrderProcessService workOrderProcessService;
@@ -88,6 +94,9 @@
    private MesWmItemConsumeService itemConsumeService;
    @Resource
    private MesWmProductProduceService productProduceService;
    @Resource
    @Lazy // 避免循环依赖:产品收货单自动生成逻辑依赖报工 Service
    private MesWmProductReceiptService productReceiptService;
    @Resource
    private MesWmProductProduceLineService produceLineService;
    @Resource
@@ -266,13 +275,17 @@
    public boolean approveFeedback(MesProFeedbackApproveReqVO reqVO) {
        // 1.1 校验存在 + 审批中状态
        MesProFeedbackDO feedback = validateFeedbackStatusApproving(reqVO.getId());
        // 1.2 校验报工数量 > 0
        if (feedback.getFeedbackQuantity() == null
                || feedback.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
        // 1.2 校验任务未完成 + 等待时长倒计时
        MesProTaskDO task = taskService.validateTaskNotFinished(feedback.getTaskId());
        MesProProcessDO process = validateWaitDuration(task);
        BigDecimal fbQty = feedback.getFeedbackQuantity() == null
                ? BigDecimal.ZERO : feedback.getFeedbackQuantity();
        // 1.3 校验报工数量:默认必须 > 0;工序配置"不需要填写报工数量"时允许为空
        if (process == null || !Boolean.FALSE.equals(process.getNeedReportQuantity())) {
            if (fbQty.compareTo(BigDecimal.ZERO) <= 0) {
                throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
            }
        }
        // 1.3 校验任务未完成
        taskService.validateTaskNotFinished(feedback.getTaskId());
        // 2.1 查询工序的关键工序标识 + 检验标识(需在 uncheckQuantity 校验之前,因为质检工序允许 uncheckQuantity > 0)
        MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId(
@@ -280,13 +293,14 @@
        boolean keyFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getKeyFlag());
        boolean checkFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getCheckFlag());
        // 2.2 非质检工序:合格品/不良品数量由质检确定;本工序不经过质检,报工未填写时默认全部为合格品
        //     (数量为空/0 时按 0 处理,避免写入 null)
        if (!checkFlag
                && feedback.getQualifiedQuantity() == null
                && feedback.getUnqualifiedQuantity() == null) {
            feedback.setQualifiedQuantity(feedback.getFeedbackQuantity());
            feedback.setQualifiedQuantity(fbQty);
            feedback.setUnqualifiedQuantity(BigDecimal.ZERO);
            feedbackMapper.updateById(new MesProFeedbackDO().setId(feedback.getId())
                    .setQualifiedQuantity(feedback.getFeedbackQuantity())
                    .setQualifiedQuantity(fbQty)
                    .setUnqualifiedQuantity(BigDecimal.ZERO));
        }
        // 2.3 非质检工序:仍有待检数量时不能审批(质检工序的 uncheckQuantity > 0 是正常状态,不做拦截)
@@ -297,8 +311,9 @@
        }
        // 3. 物料消耗:倒冲工序按 BOM 自动扣料;非倒冲工序跳过(物料走手工领料/投料)
        //    报工数量为空/0 时跳过倒冲扣料,避免空数量误扣线边库
        boolean backflushFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getBackflushFlag());
        if (backflushFlag) {
        if (backflushFlag && fbQty.compareTo(BigDecimal.ZERO) > 0) {
            // 3.1 校验倒冲工序必须配置 BOM 消耗,避免静默不扣料
            List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderId(
                    feedback.getWorkOrderId());
@@ -316,6 +331,14 @@
        feedbackMapper.updateById(new MesProFeedbackDO().setId(reqVO.getId())
                .setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus())
                .setUncheckQuantity(BigDecimal.ZERO));
        // 5.1 报工数量为空/0("不需要填写报工数量"的工序):无产出可入库,仅更新任务进度并结束
        if (fbQty.compareTo(BigDecimal.ZERO) <= 0) {
            taskService.updateProducedQuantity(feedback.getTaskId(),
                    BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO);
            checkAndPublishProductionFinished(feedback.getWorkOrderId());
            return true;
        }
        // 6. 根据是否需要检验,决定产出和入库方式
        // 6.2 调整 只要审批成功直接更新
@@ -341,6 +364,8 @@
            // 6.2 无需检验:生成产出单(按合格/不合格拆行),直接完成入库
            MesWmProductProduceDO produce = productProduceService.generateProductProduce(feedback, false, reqVO);
            productProduceService.finishProductProduce(produce.getId());
            // 6.3 报工产出定稿后自动生成产品入库单(一次报工=一条,幂等)
            productReceiptService.createProductReceiptByFeedback(feedback.getId());
        }
@@ -477,14 +502,47 @@
        if (routeProcess == null) {
            throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID);
        }
        // 4.1 校验数量:无论是否需要检验,报工只需填报工数量,且必须 > 0
        // 4. 校验等待时长倒计时:工序配置了等待时长(waitDuration > 0)时,需先开始倒计时并等待结束
        MesProProcessDO process = validateWaitDuration(task);
        // 4.1 校验数量:默认必须 > 0;工序配置"不需要填写报工数量"时允许为空
        // 合格品/不良品数量由质检确定(非质检工序在审批时默认全部为合格品)
        if (reqVO.getFeedbackQuantity() == null
                || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
        if (process == null || !Boolean.FALSE.equals(process.getNeedReportQuantity())) {
            if (reqVO.getFeedbackQuantity() == null
                    || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
                throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
            }
        }
        return task;
    }
    /**
     * 校验工序等待时长倒计时(报工前置条件)
     *
     * <p>读取任务关联的工序主数据 waitDuration(小时):若未配置或为 0 则直接返回;配置了则需要
     * 任务已点击"开始倒计时"且倒计时已结束,否则抛出对应异常。返回工序主数据,供调用方复用
     * needReportQuantity 等字段。
     *
     * @param task 生产任务
     * @return 工序主数据
     */
    private MesProProcessDO validateWaitDuration(MesProTaskDO task) {
        MesProProcessDO process = processService.getProcess(task.getProcessId());
        if (process == null || process.getWaitDuration() == null
                || process.getWaitDuration().compareTo(BigDecimal.ZERO) <= 0) {
            return process; // 未配置等待时长,无需倒计时
        }
        if (task.getWaitStartTime() == null) {
            throw exception(PRO_TASK_WAIT_NOT_STARTED); // 请先点击"开始倒计时"
        }
        LocalDateTime readyTime = task.getWaitStartTime()
                .plusMinutes((long) (process.getWaitDuration().doubleValue() * 60));
        if (LocalDateTime.now().isBefore(readyTime)) {
            Duration remaining = Duration.between(LocalDateTime.now(), readyTime);
            throw exception(PRO_TASK_WAIT_NOT_ELAPSED,
                    remaining.toHours() + "小时" + remaining.toMinutesPart() + "分钟");
        }
        return process;
    }
    @Override
@@ -504,12 +562,16 @@
        if (routeProcess == null) {
            throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID);
        }
        // 3. 校验数量:只填报工数量,且必须 > 0(合格品/不良品数量由质检确定)
        if (reqVO.getFeedbackQuantity() == null
                || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
        // 3. 校验等待时长倒计时:工序配置了等待时长(waitDuration > 0)时,需先开始倒计时并等待结束
        MesProProcessDO process = validateWaitDuration(task);
        // 4. 校验数量:默认必须 > 0;工序配置"不需要填写报工数量"时允许为空(合格品/不良品数量由质检确定)
        if (process == null || !Boolean.FALSE.equals(process.getNeedReportQuantity())) {
            if (reqVO.getFeedbackQuantity() == null
                    || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
                throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
            }
        }
        // 4. 构建报工DO
        // 5. 构建报工DO
        MesProFeedbackDO feedback = new MesProFeedbackDO()
                .setCode(autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_FEEDBACK_CODE.getCode()))
                .setType(MesProFeedbackTypeEnum.SELF.getType())