| | |
| | | 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 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; |
| | |
| | | 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; |
| | |
| | | @Resource |
| | | private MesProRouteProcessService routeProcessService; |
| | | @Resource |
| | | private MesProProcessService processService; |
| | | @Resource |
| | | private MesProWorkOrderBomService workOrderBomService; |
| | | @Resource |
| | | private MesProWorkOrderProcessService workOrderProcessService; |
| | |
| | | private MesWmItemConsumeService itemConsumeService; |
| | | @Resource |
| | | private MesWmProductProduceService productProduceService; |
| | | @Resource |
| | | @Lazy // 避免循环依赖:产品收货单自动生成逻辑依赖报工 Service |
| | | private MesWmProductReceiptService productReceiptService; |
| | | @Resource |
| | | private MesWmProductProduceLineService produceLineService; |
| | | @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, |
| | |
| | | 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( |
| | | 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 非质检工序:合格品/不良品数量由质检确定;本工序不经过质检,报工未填写时默认全部为合格品 |
| | | // (数量为空/0 时按 0 处理,避免写入 null) |
| | | if (!checkFlag |
| | | && feedback.getQualifiedQuantity() == null |
| | | && feedback.getUnqualifiedQuantity() == null) { |
| | | feedback.setQualifiedQuantity(fbQty); |
| | | feedback.setUnqualifiedQuantity(BigDecimal.ZERO); |
| | | feedbackMapper.updateById(new MesProFeedbackDO().setId(feedback.getId()) |
| | | .setQualifiedQuantity(fbQty) |
| | | .setUnqualifiedQuantity(BigDecimal.ZERO)); |
| | | } |
| | | // 2.3 非质检工序:仍有待检数量时不能审批(质检工序的 uncheckQuantity > 0 是正常状态,不做拦截) |
| | | if (!checkFlag |
| | | && feedback.getUncheckQuantity() != null |
| | | && feedback.getUncheckQuantity().compareTo(BigDecimal.ZERO) > 0) { |
| | |
| | | } |
| | | |
| | | // 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()); |
| | |
| | | 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 调整 只要审批成功直接更新 |
| | |
| | | // 6.2 无需检验:生成产出单(按合格/不合格拆行),直接完成入库 |
| | | MesWmProductProduceDO produce = productProduceService.generateProductProduce(feedback, false, reqVO); |
| | | productProduceService.finishProductProduce(produce.getId()); |
| | | // 6.3 报工产出定稿后自动生成产品入库单(一次报工=一条,幂等) |
| | | productReceiptService.createProductReceiptByFeedback(feedback.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 |
| | | // 4. 校验等待时长倒计时:工序配置了等待时长(waitDuration > 0)时,需先开始倒计时并等待结束 |
| | | MesProProcessDO process = validateWaitDuration(task); |
| | | // 4.1 校验数量:默认必须 > 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); |
| | | } |
| | | } 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); |
| | | } |
| | | } |
| | | |
| | | 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 |
| | |
| | | if (routeProcess == null) { |
| | | throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID); |
| | | } |
| | | // 3. 校验数量 |
| | | boolean checkFlag = Boolean.TRUE.equals(routeProcess.getCheckFlag()); |
| | | if (checkFlag) { |
| | | // 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); |
| | | } |
| | | } 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); |
| | | } |
| | | } |
| | | // 4. 构建报工DO |
| | | // 5. 构建报工DO |
| | | MesProFeedbackDO feedback = new MesProFeedbackDO() |
| | | .setCode(autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_FEEDBACK_CODE.getCode())) |
| | | .setType(MesProFeedbackTypeEnum.SELF.getType()) |
| | |
| | | .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() |