云
17 小时以前 ac2e2b5af3726ee64dc37c3ebf26989a0346f22a
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -11,6 +11,8 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.*;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackTypeEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
@@ -110,6 +112,8 @@
    private MesMdmItemService mdmItemService;
    @Resource
    private MdmItemBatchConfigApi itemBatchConfigApi;
    @Resource
    private MesQcTemplateItemService templateItemService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -263,6 +267,7 @@
        if (!Objects.equals(feedback.getStatus(), MesProFeedbackStatusEnum.PREPARE.getStatus()))
            throw exception(PRO_FEEDBACK_NOT_PREPARE);
        MesProTaskDO task = feedbackParamService.lockTask(feedback.getTaskId());
        validateTaskAssignee(task);
        String snapshot = feedbackParamService.complete(task, feedback.getParamSnapshot());
        MesProFeedbackDO update = new MesProFeedbackDO().setId(id)
                .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus());
@@ -271,9 +276,10 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void rejectFeedback(Long id) {
        // 1. 校验存在 + 审批中状态
        MesProFeedbackDO feedback = validateFeedbackStatusApproving(id);
        MesProFeedbackDO feedback = lockFeedbackStatusApproving(id);
        // 1.1 校验当前登录人是该报工单的指定审核人
        validateCurrentUserIsApprover(feedback);
@@ -286,7 +292,7 @@
    @Transactional(rollbackFor = Exception.class)
    public boolean approveFeedback(MesProFeedbackApproveReqVO reqVO) {
        // 1.1 校验存在 + 审批中状态
        MesProFeedbackDO feedback = validateFeedbackStatusApproving(reqVO.getId());
        MesProFeedbackDO feedback = lockFeedbackStatusApproving(reqVO.getId());
        // 1.1.1 校验当前登录人是该报工单的指定审核人
        validateCurrentUserIsApprover(feedback);
        // 1.2 校验报工数量 > 0
@@ -297,11 +303,10 @@
        // 1.3 校验任务未完成
        taskService.validateTaskNotFinished(feedback.getTaskId());
        // 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.1 查询工序的检验标识(工单工序快照优先,需在 uncheckQuantity 校验之前,因为质检工序允许 uncheckQuantity > 0)
        ProcessFlags processFlags = resolveProcessFlags(feedback.getRouteId(), feedback.getWorkOrderId(),
                feedback.getProcessId());
        boolean checkFlag = processFlags.checkFlag();
        // 2.2 非质检工序:仍有待检数量时不能审批(质检工序的 uncheckQuantity > 0 是正常状态,不做拦截)
        if (!checkFlag
                && feedback.getUncheckQuantity() != null
@@ -309,8 +314,25 @@
            throw exception(PRO_FEEDBACK_UNCHECK_QUANTITY_EXISTS, feedback.getUncheckQuantity());
        }
        if (checkFlag) {
            try {
                templateItemService.getRequiredTemplateByItemIdAndType(feedback.getItemId(), MesQcTypeEnum.IPQC.getType());
            } catch (ServiceException e) {
                if (!Objects.equals(e.getCode(), QC_NO_TEMPLATE.getCode())) throw e;
                MesMdmItemDO item = mdmItemService.getItem(feedback.getItemId());
                String name = item != null ? item.getName() + "(" + item.getCode() + ")" : "ID " + feedback.getItemId();
                throw exception(PRO_FEEDBACK_PARAM_INVALID, "物料【" + name + "】未配置启用的过程检验模板,请先配置后再审批");
            }
        } else {
            BigDecimal qualified = ObjectUtil.defaultIfNull(feedback.getQualifiedQuantity(), BigDecimal.ZERO);
            BigDecimal unqualified = ObjectUtil.defaultIfNull(feedback.getUnqualifiedQuantity(), BigDecimal.ZERO);
            if (qualified.signum() < 0 || unqualified.signum() < 0
                    || qualified.add(unqualified).compareTo(feedback.getFeedbackQuantity()) != 0) {
                throw exception(PRO_FEEDBACK_PARAM_INVALID, "合格数和不合格数必须非负且合计等于报工数量");
            }
        }
        // 3. 物料消耗:倒冲工序按 BOM 自动扣料;非倒冲工序跳过(物料走手工领料/投料)
        boolean backflushFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getBackflushFlag());
        boolean backflushFlag = processFlags.backflushFlag();
        if (backflushFlag) {
            // 3.1 校验当前倒冲工序必须配置投料 BOM,避免静默不扣料
            List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderIdAndProcessId(
@@ -327,24 +349,7 @@
            }
        }
        // 5. 先更新报工状态为已完成(确保 checkAndFinishMps 检查时当前报工已标记完成)
        feedbackMapper.updateById(new MesProFeedbackDO().setId(reqVO.getId())
                .setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus())
                .setUncheckQuantity(BigDecimal.ZERO));
        // 6. 根据是否需要检验,决定产出和入库方式
        // 6.2 调整 只要审批成功直接更新
        // 关键工序:更新任务 + 工单的已生产数量;非关键工序:只更新任务的已生产数量(工单只看关键工序产出)
        if (keyFlag) {
            updateTaskAndWorkOrderByFeedback(feedback);
        } else {
            taskService.updateProducedQuantity(feedback.getTaskId(),
                    feedback.getFeedbackQuantity(),
                    ObjectUtil.defaultIfNull(feedback.getQualifiedQuantity(), BigDecimal.ZERO),
                    ObjectUtil.defaultIfNull(feedback.getUnqualifiedQuantity(), BigDecimal.ZERO));
                checkAndPublishProductionFinished(feedback.getWorkOrderId());
        }
        // 4. 先生成产出;进度仅在产出完成且报工状态变为完成后更新。
        if (checkFlag) {
            // 6.1 需要检验:生成产出单(质量状态=待检验),更新报工状态为待检验,等质检完成回调后再入库
            // 完成时回调见:MesQcIpqcServiceImpl#finishIpqc → splitPendingAndFinishProduce + updateProFeedbackWhenIpqcFinish
@@ -356,6 +361,9 @@
            // 6.2 无需检验:生成产出单(按合格/不合格拆行),直接完成入库
            MesWmProductProduceDO produce = productProduceService.generateProductProduce(feedback, false, reqVO);
            productProduceService.finishProductProduce(produce.getId());
            feedbackMapper.updateById(new MesProFeedbackDO().setId(reqVO.getId())
                    .setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus()).setUncheckQuantity(BigDecimal.ZERO));
            updateTaskAndWorkOrderByFeedback(feedback);
        }
@@ -375,11 +383,15 @@
     * @param feedback 报工单
     */
    private void updateTaskAndWorkOrderByFeedback(MesProFeedbackDO feedback) {
        // 1. 查询该报工单关联的所有产出单行,按质量状态聚合数量
        // 1. 查询该报工单关联的产出单行,只按主产出物料(报工物料)聚合合格/不合格数量,
        //    联产出物料单独检验,不计入任务进度
        BigDecimal qualifiedQty = BigDecimal.ZERO;
        BigDecimal unqualifiedQty = BigDecimal.ZERO;
        List<MesWmProductProduceLineDO> lines = produceLineService.getProductProduceLineListByFeedbackId(feedback.getId());
        for (MesWmProductProduceLineDO line : lines) {
            if (!ObjUtil.equal(line.getItemId(), feedback.getItemId())) {
                continue;
            }
            if (ObjUtil.equal(line.getQualityStatus(), MesWmQualityStatusEnum.FAIL.getStatus())) {
                unqualifiedQty = unqualifiedQty.add(line.getQuantity());
            }
@@ -394,7 +406,24 @@
        // 3. 更新订单的已生产数量
        // 新增逻辑:只有在该笔订单最后一个工序才更新生产订单的已生产数量
        MesProWorkOrderProcessDO lastProcess = workOrderProcessService.getLastWorkOrderProcessByWorkOrderId(feedback.getWorkOrderId());
        if (lastProcess != null && ObjUtil.equal(lastProcess.getProcessId(), feedback.getProcessId())) {
        Long lastProcessId;
        if (lastProcess != null) {
            lastProcessId = lastProcess.getProcessId();
        } else {
            // 旧工单可能没有保存工单工序:与排产一致回退到工艺路线工序。
            List<MesProRouteProcessDO> routeProcesses = routeProcessService.getRouteProcessListByRouteId(feedback.getRouteId());
            if (CollUtil.isEmpty(routeProcesses) || routeProcesses.stream().anyMatch(process -> process.getSort() == null)) {
                throw exception(PRO_FEEDBACK_PARAM_INVALID, "无法确定末道工序,请检查工单和工艺路线的工序顺序");
            }
            int maxSort = routeProcesses.stream().mapToInt(MesProRouteProcessDO::getSort).max().orElseThrow();
            List<MesProRouteProcessDO> lastProcesses = routeProcesses.stream()
                    .filter(process -> process.getSort() == maxSort).toList();
            if (lastProcesses.size() != 1 || lastProcesses.get(0).getProcessId() == null) {
                throw exception(PRO_FEEDBACK_PARAM_INVALID, "无法唯一确定末道工序,请检查工艺路线的工序顺序");
            }
            lastProcessId = lastProcesses.get(0).getProcessId();
        }
        if (ObjUtil.equal(lastProcessId, feedback.getProcessId())) {
            workOrderService.updateProducedQuantity(feedback.getWorkOrderId(),
                    feedback.getFeedbackQuantity());
            // 4. 更新 MPS 的已生产数量(如果是销售订单来源)
@@ -447,6 +476,14 @@
        return feedback;
    }
    private MesProFeedbackDO lockFeedbackStatusApproving(Long id) {
        MesProFeedbackDO feedback = feedbackMapper.selectOne(new LambdaQueryWrapperX<MesProFeedbackDO>()
                .eq(MesProFeedbackDO::getId, id).last("FOR UPDATE"));
        if (feedback == null) throw exception(PRO_FEEDBACK_NOT_EXISTS);
        if (ObjUtil.notEqual(feedback.getStatus(), MesProFeedbackStatusEnum.APPROVING.getStatus()))
            throw exception(PRO_FEEDBACK_NOT_APPROVING);
        return feedback;
    }
    private MesProFeedbackDO validateFeedbackStatusApproving(Long id) {
        MesProFeedbackDO feedback = validateFeedbackExists(id);
        if (ObjUtil.notEqual(feedback.getStatus(), MesProFeedbackStatusEnum.APPROVING.getStatus())) {
@@ -473,6 +510,22 @@
    }
    /**
     * 校验当前登录人是否为任务的报工人;任务未配置报工人时无人可报工
     */
    private void validateTaskAssignee(MesProTaskDO task) {
        Long loginUserId = getLoginUserId();
        if (loginUserId == null) {
            throw exception(PRO_FEEDBACK_NOT_ASSIGNED);
        }
        String current = String.valueOf(loginUserId);
        boolean assigned = StrUtil.split(task.getAssigneeUserIds(), ',')
                .stream().map(String::trim).anyMatch(current::equals);
        if (!assigned) {
            throw exception(PRO_FEEDBACK_NOT_ASSIGNED);
        }
    }
    /**
     * 校验报工单的业务数据(创建 & 修改共用)
     *
     * <p>以生产任务为核心:前端只需传 taskId,workOrderId、routeId、processId、itemId、workstationId
@@ -489,6 +542,8 @@
        if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) {
            throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED);
        }
        // 校验当前登录人是该任务的报工人
        validateTaskAssignee(task);
        // 2. 从任务自动填充关联字段(前端无需传入,后端统一从任务获取)
        reqVO.setWorkOrderId(task.getWorkOrderId());
@@ -497,14 +552,9 @@
        reqVO.setItemId(task.getItemId());
        reqVO.setWorkstationId(task.getWorkstationId());
        // 3. 校验工艺路线 + 工序配置有效
        MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId(
                task.getRouteId(), task.getProcessId());
        if (routeProcess == null) {
            throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID);
        }
        // 3. 校验工单工序 / 工艺路线工序配置有效
        boolean checkFlag = resolveProcessFlags(task.getRouteId(), task.getWorkOrderId(), task.getProcessId()).checkFlag();
        // 4.1 校验数量
        boolean checkFlag = Boolean.TRUE.equals(routeProcess.getCheckFlag());
        if (checkFlag) {
            // 需要检验:只需填报工数量,且必须 > 0
            if (reqVO.getFeedbackQuantity() == null
@@ -523,6 +573,30 @@
        return task;
    }
    /**
     * 解析工序的检验标识与倒冲标识
     *
     * <p>工单工序快照(mes_pro_work_order_process)是下单时确认的权威配置,优先使用;
     * 工单未维护该工序时再回退到工艺路线模板(mes_pro_route_process)。
     */
    private ProcessFlags resolveProcessFlags(Long routeId, Long workOrderId, Long processId) {
        MesProWorkOrderProcessDO workOrderProcess = workOrderId != null && processId != null
                ? workOrderProcessService.getWorkOrderProcessByWorkOrderIdAndProcessId(workOrderId, processId) : null;
        MesProRouteProcessDO routeProcess = routeId != null && processId != null
                ? routeProcessService.getRouteProcessByRouteIdAndProcessId(routeId, processId) : null;
        if (workOrderProcess == null && routeProcess == null) {
            throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID);
        }
        Boolean checkFlag = workOrderProcess != null && workOrderProcess.getCheckFlag() != null
                ? workOrderProcess.getCheckFlag() : routeProcess != null ? routeProcess.getCheckFlag() : null;
        Boolean backflushFlag = workOrderProcess != null && workOrderProcess.getBackflushFlag() != null
                ? workOrderProcess.getBackflushFlag() : routeProcess != null ? routeProcess.getBackflushFlag() : null;
        return new ProcessFlags(Boolean.TRUE.equals(checkFlag), Boolean.TRUE.equals(backflushFlag));
    }
    private record ProcessFlags(boolean checkFlag, boolean backflushFlag) {
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesProFeedbackQuickCreateRespVO quickCreateFeedback(MesProFeedbackQuickCreateReqVO reqVO) {
@@ -533,15 +607,12 @@
        if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) {
            throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED);
        }
        // 校验当前登录人是该任务的报工人
        validateTaskAssignee(task);
        // 2. 校验工艺路线工序配置
        MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId(
                task.getRouteId(), task.getProcessId());
        if (routeProcess == null) {
            throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID);
        }
        // 2. 校验工单工序 / 工艺路线工序配置
        boolean checkFlag = resolveProcessFlags(task.getRouteId(), task.getWorkOrderId(), task.getProcessId()).checkFlag();
        // 3. 校验数量
        boolean checkFlag = Boolean.TRUE.equals(routeProcess.getCheckFlag());
        if (checkFlag) {
            if (reqVO.getFeedbackQuantity() == null
                    || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
@@ -633,28 +704,51 @@
                                                BigDecimal qualifiedQty, BigDecimal unqualifiedQty,
                                                BigDecimal laborScrapQty, BigDecimal materialScrapQty, BigDecimal otherScrapQty) {
        // 1. 校验报工单存在且为待检验状态
        MesProFeedbackDO feedback = validateFeedbackExists(feedbackId);
        MesProFeedbackDO feedback = feedbackMapper.selectOne(new LambdaQueryWrapperX<MesProFeedbackDO>()
                .eq(MesProFeedbackDO::getId, feedbackId).last("FOR UPDATE"));
        if (feedback == null) throw exception(PRO_FEEDBACK_NOT_EXISTS);
        if (ObjUtil.notEqual(feedback.getStatus(), MesProFeedbackStatusEnum.UNCHECK.getStatus())) {
            throw exception(PRO_FEEDBACK_NOT_UNCHECK);
        }
        // 2. 拆分待检产出行(合格/不合格),生成明细,完成产出入库
        // 2. 拆分当前检验的产出行(合格/不合格),生成明细;全部物料检验完成才完成产出入库
        productProduceService.splitPendingAndFinishProduce(feedbackId, sourceLineId, qualifiedQty, unqualifiedQty);
        // 3. 回写合格/不合格/废品数量,更新状态为已完成
        feedbackMapper.updateById(new MesProFeedbackDO().setId(feedbackId)
                .setQualifiedQuantity(qualifiedQty)
                .setUnqualifiedQuantity(unqualifiedQty)
                .setUncheckQuantity(BigDecimal.ZERO)
                .setLaborScrapQuantity(laborScrapQty)
                .setMaterialScrapQuantity(materialScrapQty)
                .setOtherScrapQuantity(otherScrapQty)
                .setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus()));
        // 3. 累加本次检验结果;报工单只记录主产出物料的合格/不合格/废品数量,
        //    联产出物料各自检验,不并入报工数量;待全部产出物料检验完成才完成报工
        List<MesWmProductProduceLineDO> produceLines = produceLineService.getProductProduceLineListByFeedbackId(feedbackId);
        MesWmProductProduceLineDO sourceLine = produceLines.stream()
                .filter(line -> ObjUtil.equal(line.getId(), sourceLineId)).findFirst().orElse(null);
        boolean mainOutput = sourceLine == null || ObjUtil.equal(sourceLine.getItemId(), feedback.getItemId());
        BigDecimal totalQualified = ObjectUtil.defaultIfNull(feedback.getQualifiedQuantity(), BigDecimal.ZERO)
                .add(mainOutput ? qualifiedQty : BigDecimal.ZERO);
        BigDecimal totalUnqualified = ObjectUtil.defaultIfNull(feedback.getUnqualifiedQuantity(), BigDecimal.ZERO)
                .add(mainOutput ? unqualifiedQty : BigDecimal.ZERO);
        BigDecimal totalLaborScrap = ObjectUtil.defaultIfNull(feedback.getLaborScrapQuantity(), BigDecimal.ZERO)
                .add(mainOutput ? laborScrapQty : BigDecimal.ZERO);
        BigDecimal totalMaterialScrap = ObjectUtil.defaultIfNull(feedback.getMaterialScrapQuantity(), BigDecimal.ZERO)
                .add(mainOutput ? materialScrapQty : BigDecimal.ZERO);
        BigDecimal totalOtherScrap = ObjectUtil.defaultIfNull(feedback.getOtherScrapQuantity(), BigDecimal.ZERO)
                .add(mainOutput ? otherScrapQty : BigDecimal.ZERO);
        boolean allInspected = produceLines.stream()
                .noneMatch(line -> ObjUtil.equal(line.getQualityStatus(), MesWmQualityStatusEnum.PENDING.getStatus()));
        MesProFeedbackDO updateObj = new MesProFeedbackDO().setId(feedbackId)
                .setQualifiedQuantity(totalQualified)
                .setUnqualifiedQuantity(totalUnqualified)
                .setLaborScrapQuantity(totalLaborScrap)
                .setMaterialScrapQuantity(totalMaterialScrap)
                .setOtherScrapQuantity(totalOtherScrap)
                .setStatus(allInspected ? MesProFeedbackStatusEnum.FINISHED.getStatus()
                        : MesProFeedbackStatusEnum.UNCHECK.getStatus());
        if (allInspected) {
            updateObj.setUncheckQuantity(BigDecimal.ZERO);
        }
        feedbackMapper.updateById(updateObj);
        // 4. 更新任务/工单的已生产数量
        feedback.setQualifiedQuantity(qualifiedQty).setUnqualifiedQuantity(unqualifiedQty);
        //5.检查并发布生产完成事件
        checkAndPublishProductionFinished(feedback.getWorkOrderId());
        // 4. 全部检验完成后回写任务/工单进度
        if (allInspected) {
            updateTaskAndWorkOrderByFeedback(feedback);
        }
    }
    @Override