| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | import java.util.ArrayList; |
| | | import java.util.Collection; |
| | | import java.util.Collections; |
| | | import java.util.Comparator; |
| | | import java.util.List; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | |
| | | private MesProWorkOrderParamRecordService paramRecordService; |
| | | @Resource |
| | | private MesProRouteProcessService routeProcessService; |
| | | @Resource |
| | | private MesProProcessService processService; |
| | | @Resource |
| | | private MesProWorkOrderBomService workOrderBomService; |
| | | @Resource |
| | |
| | | private cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi processInstanceApi; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long createFeedback(MesProFeedbackSaveReqVO createReqVO) { |
| | | // 1. 校验 |
| | | MesProTaskDO task = validateFeedbackData(createReqVO); |
| | |
| | | .setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus()) |
| | | .setItemId(task.getItemId()); |
| | | feedbackMapper.insert(feedback); |
| | | // 3. 保存参数值;必填参数校验失败时整体回滚,避免残留草稿 |
| | | saveParamValues(task.getId(), createReqVO.getParamValues()); |
| | | return feedback.getId(); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void updateFeedback(MesProFeedbackSaveReqVO updateReqVO) { |
| | | // 1.1 校验存在 + 草稿状态 |
| | | validateFeedbackStatusPrepare(updateReqVO.getId()); |
| | |
| | | MesProFeedbackDO updateObj = BeanUtils.toBean(updateReqVO, MesProFeedbackDO.class) |
| | | .setItemId(task.getItemId()); |
| | | feedbackMapper.updateById(updateObj); |
| | | // 3. 保存参数值;必填参数校验失败时整体回滚 |
| | | saveParamValues(task.getId(), updateReqVO.getParamValues()); |
| | | } |
| | | |
| | | private void saveParamValues(Long taskId, List<MesProFeedbackSaveReqVO.ParamValue> paramValues) { |
| | | // 即使参数为空也调用更新,以便触发必填参数校验(任务绑定了参数时必须填写) |
| | | if (CollUtil.isEmpty(paramValues)) { |
| | | paramRecordService.updateTaskParamValues(taskId, null, null); |
| | | return; |
| | | } |
| | | paramRecordService.updateTaskParamValues(taskId, |
| | |
| | | Long id = reqVO.getId(); |
| | | // 1. 校验存在 + 草稿状态 |
| | | MesProFeedbackDO feedback = validateFeedbackStatusPrepare(id); |
| | | // 1.1 顺序报工校验:同一生产工单内,上一道工序必须已报工完成,否则不允许提交 |
| | | if (feedback.getTaskId() != null) { |
| | | MesProTaskDO submitTask = taskService.getTask(feedback.getTaskId()); |
| | | if (submitTask != null) { |
| | | validatePreviousProcessFinished(submitTask); |
| | | } |
| | | } |
| | | |
| | | MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(feedback.getWorkOrderId()); |
| | | |
| | |
| | | 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 是正常状态,不做拦截) |
| | | // 2.2 非质检工序:合格品/不良品数量由质检确定;本工序不经过质检,报工未填写时默认全部为合格品 |
| | | if (!checkFlag |
| | | && feedback.getQualifiedQuantity() == null |
| | | && feedback.getUnqualifiedQuantity() == null) { |
| | | feedback.setQualifiedQuantity(feedback.getFeedbackQuantity()); |
| | | feedback.setUnqualifiedQuantity(BigDecimal.ZERO); |
| | | feedbackMapper.updateById(new MesProFeedbackDO().setId(feedback.getId()) |
| | | .setQualifiedQuantity(feedback.getFeedbackQuantity()) |
| | | .setUnqualifiedQuantity(BigDecimal.ZERO)); |
| | | } |
| | | // 2.3 非质检工序:仍有待检数量时不能审批(质检工序的 uncheckQuantity > 0 是正常状态,不做拦截) |
| | | if (!checkFlag |
| | | && feedback.getUncheckQuantity() != null |
| | | && feedback.getUncheckQuantity().compareTo(BigDecimal.ZERO) > 0) { |
| | | throw exception(PRO_FEEDBACK_UNCHECK_QUANTITY_EXISTS, feedback.getUncheckQuantity()); |
| | | } |
| | | |
| | | // 3. 物料消耗:倒冲工序按 BOM 自动扣料;非倒冲工序跳过(物料走手工领料/投料) |
| | | // 3. 物料消耗:倒冲工序按本工序投料 BOM 自动扣料;非倒冲工序跳过(物料走手工领料/投料) |
| | | boolean backflushFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getBackflushFlag()); |
| | | if (backflushFlag) { |
| | | // 3.1 校验倒冲工序必须配置 BOM 消耗,避免静默不扣料 |
| | | List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderId( |
| | | feedback.getWorkOrderId()); |
| | | // 3.1 校验倒冲工序必须配置本工序的投料 BOM,避免静默不扣料 |
| | | // (只校验当前报工工序的投料,其他工序投料由各自工序报工时扣除) |
| | | List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderIdAndProcessId( |
| | | feedback.getWorkOrderId(), feedback.getProcessId()); |
| | | if (CollUtil.isEmpty(boms)) { |
| | | throw exception(PRO_FEEDBACK_BACKFLUSH_BOM_REQUIRED); |
| | | } |
| | | // 3.2 生成消耗记录并执行扣减(BOM × 报工数量,FIFO 扣线边库) |
| | | // 3.2 生成消耗记录并执行扣减(本工序投料用量比例 × 报工数量,FIFO 扣线边库) |
| | | MesWmItemConsumeDO itemConsume = itemConsumeService.generateItemConsume(feedback); |
| | | if (itemConsume != null) { |
| | | itemConsumeService.finishItemConsume(itemConsume.getId()); |
| | |
| | | if (ObjUtil.equal(line.getQualityStatus(), MesWmQualityStatusEnum.PASS.getStatus())) { |
| | | qualifiedQty = qualifiedQty.add(line.getQuantity()); |
| | | } |
| | | } |
| | | // 1.1 兜底:关键非质检工序审批时产出单行尚未生成,直接采用报工单上的合格/不良数量 |
| | | // (非质检工序默认全部为合格品;质检工序的合格/不良在质检回调后按产出行聚合) |
| | | if (CollUtil.isEmpty(lines)) { |
| | | qualifiedQty = ObjectUtil.defaultIfNull(feedback.getQualifiedQuantity(), BigDecimal.ZERO); |
| | | unqualifiedQty = ObjectUtil.defaultIfNull(feedback.getUnqualifiedQuantity(), BigDecimal.ZERO); |
| | | } |
| | | |
| | | // 2. 更新任务的已生产/合格/不合格数量 |
| | |
| | | if (routeProcess == null) { |
| | | throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID); |
| | | } |
| | | // 4.1 校验数量 |
| | | boolean checkFlag = Boolean.TRUE.equals(routeProcess.getCheckFlag()); |
| | | if (checkFlag) { |
| | | // 需要检验:只需填报工数量,且必须 > 0 |
| | | if (reqVO.getFeedbackQuantity() == null |
| | | || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE); |
| | | } |
| | | } else { |
| | | // 不需检验:需填合格品 + 不良品数量,合计 > 0 |
| | | BigDecimal qualified = ObjectUtil.defaultIfNull(reqVO.getQualifiedQuantity(), BigDecimal.ZERO); |
| | | BigDecimal unqualified = ObjectUtil.defaultIfNull(reqVO.getUnqualifiedQuantity(), BigDecimal.ZERO); |
| | | if (qualified.add(unqualified).compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw exception(PRO_FEEDBACK_QUALIFIED_UNQUALIFIED_REQUIRED); |
| | | } |
| | | // 4.1 校验数量:无论是否需要检验,报工只需填报工数量,且必须 > 0 |
| | | // 合格品/不良品数量由质检确定(非质检工序在审批时默认全部为合格品) |
| | | if (reqVO.getFeedbackQuantity() == null |
| | | || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE); |
| | | } |
| | | |
| | | return task; |
| | | } |
| | | |
| | | /** |
| | | * 顺序报工校验:同一生产工单内,上一道工序必须已报工完成,否则不允许报本道工序 |
| | | * |
| | | * <p>判定口径:上一道工序的有效任务(未取消)累计审批通过的产量已达到其排产数量, |
| | | * 即任一有效任务 produced_quantity < quantity 都视为未完成并拦截。 |
| | | * 之所以不依赖任务 status:任务状态只有整单完工/取消时才置终态 |
| | | * (finishTaskByOrderId / cancelTaskByOrderId),无法作为"单道工序已完成"的依据, |
| | | * produced_quantity 是唯一实时可靠的进度信号。 |
| | | * |
| | | * @param task 当前待报工任务 |
| | | */ |
| | | private void validatePreviousProcessFinished(MesProTaskDO task) { |
| | | if (task == null || task.getWorkOrderId() == null || task.getProcessId() == null) { |
| | | return; // 数据不完整,无法推导顺序,放行 |
| | | } |
| | | // 1. 当前任务所属工序在工单工序链(按 sort 升序)中的位置 |
| | | List<MesProWorkOrderProcessDO> processes = workOrderProcessService |
| | | .getWorkOrderProcessListByWorkOrderId(task.getWorkOrderId()); |
| | | if (CollUtil.isEmpty(processes)) { |
| | | return; // 工单未配置工序,放行 |
| | | } |
| | | // 拷贝后再排序,避免修改调用方返回的(可能是不可变)集合 |
| | | processes = new ArrayList<>(processes); |
| | | processes.sort(Comparator.comparing(MesProWorkOrderProcessDO::getSort, |
| | | Comparator.nullsLast(Comparator.naturalOrder()))); |
| | | int index = -1; |
| | | for (int i = 0; i < processes.size(); i++) { |
| | | if (ObjUtil.equal(processes.get(i).getProcessId(), task.getProcessId())) { |
| | | index = i; |
| | | break; |
| | | } |
| | | } |
| | | if (index <= 0) { |
| | | return; // 当前工序不在链中(数据异常,放行)或本身就是第一道工序,无需前置 |
| | | } |
| | | Long prevProcessId = processes.get(index - 1).getProcessId(); |
| | | if (prevProcessId == null) { |
| | | return; |
| | | } |
| | | // 2. 上一道工序的有效任务(排除已取消)累计待完成量 |
| | | List<MesProTaskDO> prevTasks = taskService.getTaskListByWorkOrderIds( |
| | | Collections.singletonList(task.getWorkOrderId())).stream() |
| | | .filter(t -> ObjUtil.equal(t.getProcessId(), prevProcessId)) |
| | | .filter(t -> !ObjUtil.equal(t.getStatus(), MesProTaskStatusEnum.CANCELED.getStatus())) |
| | | .toList(); |
| | | if (CollUtil.isEmpty(prevTasks)) { |
| | | return; // 上一道工序无有效生产任务(含全部取消),视为无前置要求,放行 |
| | | } |
| | | BigDecimal remaining = BigDecimal.ZERO; |
| | | for (MesProTaskDO prevTask : prevTasks) { |
| | | BigDecimal qty = prevTask.getQuantity() == null ? BigDecimal.ZERO : prevTask.getQuantity(); |
| | | BigDecimal produced = prevTask.getProducedQuantity() == null ? BigDecimal.ZERO : prevTask.getProducedQuantity(); |
| | | if (produced.compareTo(qty) < 0) { |
| | | remaining = remaining.add(qty.subtract(produced)); |
| | | } |
| | | } |
| | | if (remaining.compareTo(BigDecimal.ZERO) <= 0) { |
| | | return; // 上一道工序已全部报满,放行 |
| | | } |
| | | // 3. 拦截并提示上一道工序名称与剩余待报数量 |
| | | MesProProcessDO prevProcess = processService.getProcess(prevProcessId); |
| | | String prevProcessName = prevProcess != null ? prevProcess.getName() : String.valueOf(prevProcessId); |
| | | throw exception(PRO_FEEDBACK_PREV_PROCESS_NOT_FINISHED, prevProcessName, remaining); |
| | | } |
| | | |
| | | @Override |
| | |
| | | if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) { |
| | | throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED); |
| | | } |
| | | // 1.1 顺序报工校验:同一生产工单内,上一道工序必须已报工完成,否则不允许报工 |
| | | validatePreviousProcessFinished(task); |
| | | |
| | | // 2. 校验工艺路线工序配置 |
| | | MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId( |
| | |
| | | if (routeProcess == null) { |
| | | throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID); |
| | | } |
| | | // 3. 校验数量 |
| | | boolean checkFlag = Boolean.TRUE.equals(routeProcess.getCheckFlag()); |
| | | if (checkFlag) { |
| | | if (reqVO.getFeedbackQuantity() == null |
| | | || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE); |
| | | } |
| | | } else { |
| | | BigDecimal qualified = ObjectUtil.defaultIfNull(reqVO.getQualifiedQuantity(), BigDecimal.ZERO); |
| | | BigDecimal unqualified = ObjectUtil.defaultIfNull(reqVO.getUnqualifiedQuantity(), BigDecimal.ZERO); |
| | | if (qualified.add(unqualified).compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw exception(PRO_FEEDBACK_QUALIFIED_UNQUALIFIED_REQUIRED); |
| | | } |
| | | // 3. 校验数量:只填报工数量,且必须 > 0(合格品/不良品数量由质检确定) |
| | | if (reqVO.getFeedbackQuantity() == null |
| | | || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE); |
| | | } |
| | | // 4. 构建报工DO |
| | | MesProFeedbackDO feedback = new MesProFeedbackDO() |
| | |
| | | .setTaskId(reqVO.getTaskId()) |
| | | .setItemId(task.getItemId()) |
| | | .setFeedbackQuantity(reqVO.getFeedbackQuantity()) |
| | | .setQualifiedQuantity(reqVO.getQualifiedQuantity()) |
| | | .setUnqualifiedQuantity(reqVO.getUnqualifiedQuantity()) |
| | | .setScheduledQuantity(task.getQuantity()) |
| | | .setFeedbackTime(LocalDateTime.now()) |
| | | .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus()) |
| | |
| | | saveParamValues(task.getId(), reqVO.getParamValues()); |
| | | // 5. 返回结果 |
| | | MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(task.getWorkOrderId()); |
| | | // 5.1 报工后剩余可报工数量 = 排产 - 已生产 - 在途(含本次报工) |
| | | BigDecimal reportable = task.getQuantity() == null ? BigDecimal.ZERO : task.getQuantity(); |
| | | BigDecimal produced = task.getProducedQuantity() == null ? BigDecimal.ZERO : task.getProducedQuantity(); |
| | | BigDecimal inTransit = BigDecimal.ZERO; |
| | | for (MesProFeedbackDO fb : getFeedbackListByTaskId(task.getId())) { |
| | | if (!ObjUtil.equal(fb.getStatus(), MesProFeedbackStatusEnum.FINISHED.getStatus())) { |
| | | inTransit = inTransit.add(fb.getFeedbackQuantity() == null ? BigDecimal.ZERO : fb.getFeedbackQuantity()); |
| | | } |
| | | } |
| | | BigDecimal reportableQuantity = reportable.subtract(produced).subtract(inTransit).max(BigDecimal.ZERO); |
| | | return new MesProFeedbackQuickCreateRespVO() |
| | | .setFeedbackId(feedback.getId()) |
| | | .setTaskStatus(task.getStatus()) |
| | | .setWorkOrderStatus(workOrder != null ? workOrder.getStatus() : null); |
| | | .setWorkOrderStatus(workOrder != null ? workOrder.getStatus() : null) |
| | | .setReportableQuantity(reportableQuantity); |
| | | } |
| | | |
| | | @Override |
| | |
| | | MesProFeedbackQuickCreateReqVO singleReq = new MesProFeedbackQuickCreateReqVO(); |
| | | singleReq.setTaskId(taskId); |
| | | singleReq.setFeedbackQuantity(reqVO.getFeedbackQuantity()); |
| | | singleReq.setQualifiedQuantity(reqVO.getQualifiedQuantity()); |
| | | singleReq.setUnqualifiedQuantity(reqVO.getUnqualifiedQuantity()); |
| | | singleReq.setRemark(reqVO.getRemark()); |
| | | MesProFeedbackQuickCreateRespVO resp = quickCreateFeedback(singleReq); |
| | | results.add(new MesProFeedbackBatchCreateRespVO.BatchResult() |