package cn.iocoder.yudao.module.mes.service.pro.feedback; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; import org.flowable.engine.repository.ProcessDefinition; import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.*; import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; 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.service.md.autocode.MesMdAutoCodeRecordService; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackAuxDO; 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.dal.dataobject.pro.workorder.MesProWorkOrderDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDO; 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.dataobject.qc.ipqc.MesQcIpqcDO; import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackAuxMapper; import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper; import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper; import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum; import cn.iocoder.yudao.module.mes.enums.pro.MesProProcessBillingTypeEnum; import cn.iocoder.yudao.module.mes.enums.pro.MesProTaskStatusEnum; import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum; import cn.iocoder.yudao.module.mes.service.qc.ipqc.MesQcIpqcService; import cn.iocoder.yudao.module.mes.service.qc.ipqc.MesQcIpqcWageFieldService; 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.pro.workorder.MesProWorkOrderProcessService; import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService; import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderParamRecordService; import cn.iocoder.yudao.module.mes.service.pro.wage.MesProFeedbackQualityResult; import cn.iocoder.yudao.module.mes.service.pro.wage.MesProFeedbackWageBreakdown; import cn.iocoder.yudao.module.mes.service.pro.wage.MesProFeedbackWageCalculator; import cn.iocoder.yudao.module.mes.service.pro.wage.MesProProcessWageRuleService; import cn.iocoder.yudao.module.mes.service.pro.worktime.MesProWorktimeService; 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.mdm.MesMdmItemService; import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO; import cn.iocoder.yudao.module.mdm.api.item.MdmItemBatchConfigApi; import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemBatchConfigRespDTO; import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi; import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsRespDTO; import cn.iocoder.yudao.module.mes.api.event.ProductionFinishedEvent; import jakarta.annotation.Resource; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.LinkedHashSet; import java.util.Map; import java.util.Objects; import java.util.Set; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; /** * MES 生产报工 Service 实现类 * * @author 超级管理员 */ @Service @Validated @lombok.extern.slf4j.Slf4j public class MesProFeedbackServiceImpl implements MesProFeedbackService { private static final String FEEDBACK_APPROVE_CATEGORY_CODE = "mes_pro_feedback_audit"; @Resource private MesProFeedbackEquipmentService equipmentService; @Resource private MesProFeedbackMapper feedbackMapper; @Resource private MesProFeedbackAuxMapper feedbackAuxMapper; @Resource private MesProWorktimeService worktimeService; @Resource private MesProFeedbackWageCalculator wageCalculator; @Resource private MesProProcessWageRuleService wageRuleService; @Resource private MesQcIpqcMapper ipqcMapper; @Resource @Lazy // IPQC 完成回写报工,避免循环依赖 private MesQcIpqcService ipqcService; @Resource @Lazy // 过程检验单工资字段值(质检填写),避免与 IPQC 主服务循环依赖 private MesQcIpqcWageFieldService ipqcWageFieldService; @Resource private MesProWorkOrderService workOrderService; @Resource @Lazy private MesProWorkOrderParamRecordService paramRecordService; @Resource private MesProRouteProcessService routeProcessService; @Resource private MesProProcessService processService; @Resource private MesProWorkOrderBomService workOrderBomService; @Resource private MesProWorkOrderProcessService workOrderProcessService; @Resource @Lazy // 避免循环依赖 private MesProTaskService taskService; @Resource private MesWmItemConsumeService itemConsumeService; @Resource private MesWmProductProduceService productProduceService; @Resource private MesWmProductProduceLineService produceLineService; @Resource private MesProMpsApi mpsApi; @Resource private MesMdAutoCodeRecordService autoCodeRecordService; @Resource private ApplicationEventPublisher eventPublisher; @Resource private MesMdmItemService mdmItemService; @Resource private MdmItemBatchConfigApi itemBatchConfigApi; @Resource private cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi processInstanceApi; @Resource private BpmProcessDefinitionService bpmProcessDefinitionService; @Override @Transactional(rollbackFor = Exception.class) public Long createFeedback(MesProFeedbackSaveReqVO createReqVO) { // 1. 校验 MesProTaskDO task = validateFeedbackData(createReqVO); // 2. 插入(主键由数据库自增,忽略请求中可能残留的 id;自动填充 itemId) MesProFeedbackDO feedback = BeanUtils.toBean(createReqVO, MesProFeedbackDO.class) .setId(null) .setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus()) .setItemId(task.getItemId()); // 2.1 计算标准工时快照与辅助工时明细(只用于考核对比,参数缺失不阻断报工) List auxList = worktimeService.calculateForFeedback(feedback, createReqVO.getAuxList()); // 2.2 按工序计费类型、单价规则、辅助计薪与工资字段快照工资,谁报的工就记在谁名下 applyProcessBilling(feedback, processService.getProcess(task.getProcessId()), auxList); feedbackMapper.insert(feedback); saveFeedbackAux(feedback.getId(), auxList); // 3. 保存参数值;必填参数校验失败时整体回滚,避免残留草稿 saveParamValues(task.getId(), createReqVO.getParamValues()); return feedback.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void updateFeedback(MesProFeedbackSaveReqVO updateReqVO) { equipmentService.lock(); // 1.1 校验存在 + 草稿状态 validateFeedbackStatusPrepare(updateReqVO.getId()); // 1.2 校验业务数据 MesProTaskDO task = validateFeedbackData(updateReqVO); // 2. 更新(自动填充 itemId) MesProFeedbackDO updateObj = BeanUtils.toBean(updateReqVO, MesProFeedbackDO.class) .setItemId(task.getItemId()); // 2.1 重新计算标准工时快照与辅助工时明细 List auxList = worktimeService.calculateForFeedback(updateObj, updateReqVO.getAuxList()); // 2.2 按工序当前的计费类型/单价规则/辅助计薪/工资字段重算工资(草稿阶段允许改工时/数量) applyProcessBilling(updateObj, processService.getProcess(task.getProcessId()), auxList); feedbackMapper.updateById(updateObj); feedbackAuxMapper.deleteByFeedbackId(updateReqVO.getId()); saveFeedbackAux(updateReqVO.getId(), auxList); equipmentService.resetSnapshot(updateReqVO.getId()); // 3. 保存参数值;必填参数校验失败时整体回滚 saveParamValues(task.getId(), updateReqVO.getParamValues()); } private void saveFeedbackAux(Long feedbackId, List auxList) { if (CollUtil.isEmpty(auxList)) { return; } auxList.forEach(aux -> { aux.setId(null).setFeedbackId(feedbackId); feedbackAuxMapper.insert(aux); }); } private void saveParamValues(Long taskId, List paramValues) { // 即使参数为空也调用更新,以便触发必填参数校验(任务绑定了参数时必须填写) if (CollUtil.isEmpty(paramValues)) { paramRecordService.updateTaskParamValues(taskId, null, null); return; } paramRecordService.updateTaskParamValues(taskId, paramValues.stream().map(MesProFeedbackSaveReqVO.ParamValue::getDetailId).toList(), paramValues.stream().map(MesProFeedbackSaveReqVO.ParamValue::getValue).toList()); } @Override @Transactional(rollbackFor = Exception.class) public void deleteFeedback(Long id) { equipmentService.lock(); // 1. 校验存在 + 草稿状态 validateFeedbackStatusPrepare(id); // 2. 删除(辅助工时明细一并删除) feedbackMapper.deleteById(id); feedbackAuxMapper.deleteByFeedbackId(id); } @Override public MesProFeedbackDO getFeedback(Long id) { return feedbackMapper.selectById(id); } @Override public List getFeedbackListByTaskIds(Collection taskIds) { if (CollUtil.isEmpty(taskIds)) { return Collections.emptyList(); } return feedbackMapper.selectListByTaskIds(taskIds); } @Override public List getFeedbackListByTaskId(Long taskId) { return feedbackMapper.selectListByTaskId(taskId); } @Override public PageResult getFeedbackPage(MesProFeedbackPageReqVO pageReqVO) { return feedbackMapper.selectPage(pageReqVO); } @Override @Transactional(rollbackFor = Exception.class) public void submitFeedback(MesProFeedbackSubmitReqVO reqVO) { equipmentService.lock(); Long id = reqVO.getId(); // 1. 校验存在 + 草稿状态 MesProFeedbackDO feedback = validateFeedbackStatusPrepare(id); equipmentService.validateSubmission(feedback); // 1.1 顺序报工校验:同一生产工单内,上一道工序必须已报工完成,否则不允许提交 if (feedback.getTaskId() != null) { MesProTaskDO submitTask = taskService.getTask(feedback.getTaskId()); if (submitTask != null) { validatePreviousProcessFinished(submitTask); } } MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(feedback.getWorkOrderId()); if (workOrder != null && Boolean.TRUE.equals(workOrder.getFeedbackApprove()) && StrUtil.isBlank(workOrder.getProcessDefinitionKey())) { throw exception(PRO_FEEDBACK_APPROVAL_PROCESS_KEY_REQUIRED); } // 1.2 报工与质检分离:提交报工不再自动生成过程检验单,质检由质检部门手工建单 if (!processInstanceApi.isGlobalAutoApprove() && workOrder != null && Boolean.TRUE.equals(workOrder.getFeedbackApprove()) && StrUtil.isNotBlank(workOrder.getProcessDefinitionKey())) { ProcessDefinition processDefinition = bpmProcessDefinitionService .getActiveProcessDefinition(workOrder.getProcessDefinitionKey()); if (processDefinition == null || bpmProcessDefinitionService .getProcessDefinitionInfoListByCategory(FEEDBACK_APPROVE_CATEGORY_CODE).stream() .noneMatch(info -> Objects.equals(info.getProcessDefinitionId(), processDefinition.getId()))) { throw exception(PRO_FEEDBACK_APPROVAL_PROCESS_KEY_INVALID); } // 2. 更新状态为审批中,并启动流程 String processInstanceId = processInstanceApi.createProcessInstance(cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId(), new cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO() .setProcessDefinitionKey(workOrder.getProcessDefinitionKey()) .setBusinessKey(String.valueOf(id)) .setVariables(Map.of("businessNo", feedback.getCode()))); feedbackMapper.updateById(new MesProFeedbackDO().setId(id) .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus()) .setProcessDefinitionKey(workOrder.getProcessDefinitionKey()) .setProcessInstanceId(processInstanceId)); } else { // 2. 判断这个报工需不需要批次号 MesProFeedbackBatchCheckRespVO mesProFeedbackBatchCheckRespVO = checkBatchRequirements(id); if (mesProFeedbackBatchCheckRespVO.getNeedBatchInput()) { if (StrUtil.isBlank(reqVO.getLotNumber())) { throw exception(MD_ITEM_BATCH_CONFIG_AT_LEAST_ONE_APPROVE_FLAG); } } // 3. 更新状态为审批中,然后直接通过审批 feedbackMapper.update(null, new LambdaUpdateWrapper() .eq(MesProFeedbackDO::getId, id) .set(MesProFeedbackDO::getStatus, MesProFeedbackStatusEnum.APPROVING.getStatus()) .set(MesProFeedbackDO::getProcessInstanceId, null) .set(MesProFeedbackDO::getProcessDefinitionKey, null)); MesProFeedbackApproveReqVO approveReqVO = cn.iocoder.yudao.framework.common.util.object.BeanUtils.toBean(reqVO, MesProFeedbackApproveReqVO.class); // 自动审批:过程检验单刚生成尚未填写,不做质检工资前置校验(质量调整留空) approveFeedback(approveReqVO, false); } } @Override @Transactional(rollbackFor = Exception.class) public void handleBpmProcessResult(Long id, Integer bpmResult,String reason) { // 1. 柔性校验:获取报工单并判断状态 MesProFeedbackDO feedback = feedbackMapper.selectById(id); if (feedback == null) { log.warn("[handleBpmProcessResult] 生产报工({}) 不存在", id); return; } if (!MesProFeedbackStatusEnum.APPROVING.getStatus().equals(feedback.getStatus())) { log.warn("[handleBpmProcessResult] 生产报工({}) 不处于审批中状态,当前状态:{}", id, feedback.getStatus()); return; } // 2. 根据审批结果进行处理 if (cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(bpmResult)) { MesProFeedbackApproveReqVO reqVO = new MesProFeedbackApproveReqVO(); reqVO.setId(id); // 判断这个报工需不需要批次号 MesProFeedbackBatchCheckRespVO mesProFeedbackBatchCheckRespVO = checkBatchRequirements(id); if (mesProFeedbackBatchCheckRespVO.getNeedBatchInput()){ if (StrUtil.isBlank(reason)){ // 报错 请在审批意见里填写批次号 throw exception(MD_ITEM_BATCH_CONFIG_AT_LEAST_ONE_APPROVE_FLAG); }else { BeanUtils.copyProperties(mesProFeedbackBatchCheckRespVO,reqVO); reqVO.setLotNumber(reason); } } approveFeedback(reqVO); } else if (cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(bpmResult) || cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(bpmResult)) { rejectFeedback(id); } } @Override @Transactional(rollbackFor = Exception.class) public void rejectFeedback(Long id) { equipmentService.lock(); // 1. 校验存在 + 审批中状态 validateFeedbackStatusApproving(id); // 2. 更新状态为草稿 feedbackMapper.updateById(new MesProFeedbackDO().setId(id) .setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus())); } @Override @Transactional(rollbackFor = Exception.class) public boolean approveFeedback(MesProFeedbackApproveReqVO reqVO) { return approveFeedback(reqVO, true); } private boolean approveFeedback(MesProFeedbackApproveReqVO reqVO, boolean enforceQualityGate) { equipmentService.lock(); // 1.1 校验存在 + 审批中状态 MesProFeedbackDO feedback = validateFeedbackStatusApproving(reqVO.getId()); equipmentService.validateApproval(feedback); try (var equipmentScope = equipmentService.beginApproval(feedback)) { return approveValidatedFeedback(reqVO, feedback, enforceQualityGate); } } private boolean approveValidatedFeedback(MesProFeedbackApproveReqVO reqVO, MesProFeedbackDO feedback, boolean enforceQualityGate) { // 1.2 校验报工数量 > 0 if (feedback.getFeedbackQuantity() == null || feedback.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) { throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE); } // 1.3 校验任务未完成 taskService.validateTaskNotFinished(feedback.getTaskId()); // 报工与质检分离:报工审批不再依赖「是否质检」开关,质量数据由质检单回填。 // 1.4 工资按报工时点的快照重算,避免工序定额事后调整影响已提交的报工 recalculateWageFromSnapshot(feedback, null); // 3. 物料消耗:只要本工序配置了生产 BOM 就自动扣料,不再依赖倒冲开关; // 未配置 BOM 的工序跳过(物料走手工领料/投料),不阻断报工审批 List boms = workOrderBomService.getWorkOrderBomListByWorkOrderIdAndProcessId( feedback.getWorkOrderId(), feedback.getProcessId()); if (CollUtil.isNotEmpty(boms)) { // 3.1 生成消耗记录并执行扣减(本工序投料用量比例 × 报工数量,FIFO 扣线边库) MesWmItemConsumeDO itemConsume = itemConsumeService.generateItemConsume(feedback); if (itemConsume != null) { itemConsumeService.finishItemConsume(itemConsume.getId()); } } // 报工与质检分开:报工审批完成即累计产量,质量数量由质检回填。 feedbackMapper.updateById(new MesProFeedbackDO().setId(reqVO.getId()) .setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus()) .setQualifiedQuantity(BigDecimal.ZERO) .setUnqualifiedQuantity(BigDecimal.ZERO) .setUncheckQuantity(feedback.getFeedbackQuantity()) .setBillingType(feedback.getBillingType()) .setWageRate(feedback.getWageRate()) .setWageRuleId(feedback.getWageRuleId()) .setWorkHours(feedback.getWorkHours()) .setWageAmount(feedback.getWageAmount()) .setAuxPaidHours(feedback.getAuxPaidHours()) .setAuxWageAmount(feedback.getAuxWageAmount()) .setQualityPassRate(feedback.getQualityPassRate()) .setQualityCoefficient(feedback.getQualityCoefficient()) .setQualityDeduction(feedback.getQualityDeduction()) .setWageBreakdown(feedback.getWageBreakdown())); feedback.setQualifiedQuantity(BigDecimal.ZERO).setUnqualifiedQuantity(BigDecimal.ZERO); productProduceService.generateProductProduce(feedback, true, reqVO); // 每道工序只累计本任务;工单与 MPS 由最后一道工序回写,与关键工序标记无关。 updateTaskAndWorkOrderByFeedback(feedback); equipmentService.afterApproved(feedback); return false; // 产出等待质检,报工产量已累计 } /** * 根据当前报工单的最终结果,更新生产任务和生产工单的进度 * *
    *
  • 使用报工数量(feedbackQuantity),累加任务和工单的已生产数量
  • *
  • 使用产出单行按 qualityStatus 聚合的合格品/不合格品数量,累加任务的合格品/不合格品数量 * (不直接用 feedback 上的数量,确保质检回调场景下数量来源正确)
  • * isPush 是否推送生产完成事件 *
* * @param feedback 报工单 */ void updateTaskAndWorkOrderByFeedback(MesProFeedbackDO feedback) { // 报工审批只累计产量;合格与不合格数量在质检完成时累计。 taskService.updateProducedQuantity(feedback.getTaskId(), feedback.getFeedbackQuantity(), BigDecimal.ZERO, BigDecimal.ZERO); // 只有最后一道工序的报工计入工单与 MPS,避免中间工序重复累计。 MesProWorkOrderProcessDO lastProcess = workOrderProcessService.getLastWorkOrderProcessByWorkOrderId(feedback.getWorkOrderId()); if (lastProcess != null && ObjUtil.equal(lastProcess.getProcessId(), feedback.getProcessId())) { workOrderService.updateProducedQuantity(feedback.getWorkOrderId(), feedback.getFeedbackQuantity()); // MPS 只记录销售订单需求量;实际超产仍由工单和产出入库记录完整保留。 allocateMpsProducedQuantity(feedback.getWorkOrderId(), feedback.getFeedbackQuantity()); } // 5. 检查并发布生产完成事件 checkAndPublishProductionFinished(feedback.getWorkOrderId()); } /** * 检查并发布生产完成事件 */ void checkAndPublishProductionFinished(Long workOrderId) { Set newlyCompletedSaleOrderIds = new LinkedHashSet<>(); for (MesProMpsRespDTO mps : mpsApi.getMpsListByWorkOrderId(workOrderId)) { if (mps.getSaleOrderId() == null || ObjUtil.equal(mps.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum.FINISHED.getStatus()) || ObjUtil.equal(mps.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum.CANCELED.getStatus())) { continue; } if (Boolean.TRUE.equals(mpsApi.checkAndFinishMps(mps.getId()))) { newlyCompletedSaleOrderIds.add(mps.getSaleOrderId()); } } for (Long saleOrderId : newlyCompletedSaleOrderIds) { if (mpsApi.checkSaleOrderProductionFinished(saleOrderId)) { eventPublisher.publishEvent(new ProductionFinishedEvent(saleOrderId, java.time.LocalDateTime.now())); } } } void allocateMpsProducedQuantity(Long workOrderId, BigDecimal actualQuantity) { BigDecimal remaining = actualQuantity; for (MesProMpsRespDTO mps : mpsApi.getMpsListByWorkOrderId(workOrderId)) { if (remaining.signum() <= 0) { break; } if (ObjUtil.equal(mps.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum.CANCELED.getStatus())) { continue; } BigDecimal planned = mps.getQuantity() == null ? BigDecimal.ZERO : mps.getQuantity(); BigDecimal produced = mps.getQuantityProduced() == null ? BigDecimal.ZERO : mps.getQuantityProduced(); BigDecimal need = planned.subtract(produced).max(BigDecimal.ZERO); BigDecimal assigned = remaining.min(need); if (assigned.signum() > 0) { mpsApi.updateMpsProducedQuantity(mps.getId(), assigned); remaining = remaining.subtract(assigned); } } if (remaining.signum() > 0) { log.info("[allocateMpsProducedQuantity] 工单({}) 超产数量({})保留在产出库存,不分配到销售订单", workOrderId, remaining); } } // ==================== 校验方法 ==================== @Override public MesProFeedbackDO validateFeedbackExists(Long id) { MesProFeedbackDO feedback = feedbackMapper.selectById(id); if (feedback == null) { throw exception(PRO_FEEDBACK_NOT_EXISTS); } return feedback; } private MesProFeedbackDO validateFeedbackStatusPrepare(Long id) { MesProFeedbackDO feedback = validateFeedbackExists(id); if (ObjUtil.notEqual(feedback.getStatus(), MesProFeedbackStatusEnum.PREPARE.getStatus())) { throw exception(PRO_FEEDBACK_NOT_PREPARE); } return feedback; } private MesProFeedbackDO validateFeedbackStatusApproving(Long id) { MesProFeedbackDO feedback = validateFeedbackExists(id); if (ObjUtil.notEqual(feedback.getStatus(), MesProFeedbackStatusEnum.APPROVING.getStatus())) { throw exception(PRO_FEEDBACK_NOT_APPROVING); } return feedback; } /** * 校验报工单的业务数据(创建 & 修改共用) * *

以生产任务为核心:前端只需传 taskId,workOrderId、routeId、processId、itemId、workstationId * 从任务自动填充,无需前端重复传入。 * * @param reqVO 报工请求 * @return 关联的生产任务 */ private MesProTaskDO validateFeedbackData(MesProFeedbackSaveReqVO reqVO) { // 1. 校验任务存在且未终态 MesProTaskDO task = taskService.validateTaskNotFinished(reqVO.getTaskId()); // 校验停工状态不能报工 if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) { throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED); } // 2. 从任务自动填充关联字段(前端无需传入,后端统一从任务获取) reqVO.setWorkOrderId(task.getWorkOrderId()); reqVO.setRouteId(task.getRouteId()); reqVO.setProcessId(task.getProcessId()); 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); } // 4.1 校验数量:无论是否需要检验,报工只需填报工数量,且必须 > 0 // 合格品/不良品数量由质检确定。 if (reqVO.getFeedbackQuantity() == null || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) { throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE); } return task; } /** * 按工序配置把计费类型、单价规则、辅助计薪与工资字段快照到报工单,并计算本次报工工资。 * *

口径: *

    *
  • 计时:主工资 = 本次报工工时(小时)× 单价(元/小时);单价大于 0 时必须填写工时
  • *
  • 计件:主工资 = 合格数量(未填时取报工数量)× 单价(元/件)
  • *
  • 辅助:辅助明细分钟数 ÷ 60 × 辅助小时单价;工序辅助小时工资兜底
  • *
  • 字段:系数连乘作用于(主工资 + 辅助工资),再叠加加减金额与计价数量
  • *
* 前端传入的 billingType/wageRate/wageAmount 一律以后端计算覆盖,防止被篡改。 * 单价为 0 表示该工序尚未定价,按 0 元计薪,不阻断报工(核算时可按人调整)。 */ private void applyProcessBilling(MesProFeedbackDO feedback, MesProProcessDO process, List auxList) { if (process == null) { throw exception(PRO_FEEDBACK_PROCESS_NOT_EXISTS, feedback.getProcessId()); } MesProFeedbackWageBreakdown breakdown = wageCalculator.calculate(process, feedback.getRouteId(), feedback.getMachineryId(), feedback.getWorkHours(), feedback.getFeedbackQuantity(), feedback.getQualifiedQuantity(), auxList, feedback.getFeedbackTime() != null ? feedback.getFeedbackTime().toLocalDate() : null, getWageFieldValues(feedback.getId())); applyWageBreakdown(feedback, breakdown); } /** * 把工资计算结果写回报工单 */ private void applyWageBreakdown(MesProFeedbackDO feedback, MesProFeedbackWageBreakdown breakdown) { if (CollUtil.isNotEmpty(breakdown.getWarnings())) { log.warn("报工工资计算提示:报工单[{}] {}", feedback.getCode(), String.join(";", breakdown.getWarnings())); } feedback.setBillingType(breakdown.getBillingType()) .setWageRate(breakdown.getWageRate()) .setWageRuleId(breakdown.getWageRuleId()) .setWorkHours(breakdown.getWorkHours()) .setAuxPaidHours(breakdown.getAuxPaidHours()) .setAuxWageAmount(breakdown.getAuxWage()) .setWageAmount(breakdown.getFinalWage()) .setQualityPassRate(breakdown.getQuality() != null ? breakdown.getQuality().getPassRate() : null) .setQualityCoefficient(breakdown.getQuality() != null ? breakdown.getQuality().getCoefficient() : null) .setQualityDeduction(breakdown.getQuality() != null && breakdown.getQuality().getDeduction() != null ? breakdown.getQuality().getDeduction() : BigDecimal.ZERO) .setWageBreakdown(JsonUtils.toJsonString(breakdown)); } /** * 取第一个设备编号作为主计价设备(快捷报工的 machineryIds 为逗号分隔) */ private static Long firstMachineryId(String machineryIds) { if (StrUtil.isBlank(machineryIds)) { return null; } for (String part : machineryIds.split(",")) { String trimmed = part.trim(); if (StrUtil.isBlank(trimmed)) { continue; } try { return Long.parseLong(trimmed); } catch (NumberFormatException ex) { return null; } } return null; } /** * 审批时按报工时点的快照重算工资,避免工序配置事后调整影响已提交的报工。 * 历史数据(无工资拆分快照)则按当前工序配置补齐。 * * @param qualityResult 过程检验汇总;空=非质检工序或自动审批,不做质量调整 */ private void recalculateWageFromSnapshot(MesProFeedbackDO feedback, MesProFeedbackQualityResult qualityResult) { BigDecimal qualifiedQuantity = qualityResult != null ? qualityResult.qualifiedQuantity() : feedback.getQualifiedQuantity(); // 质检部门在过程检验单上填写的工资字段值,优先于工序固定值 Map wageFieldValues = getWageFieldValues(feedback.getId()); MesProFeedbackWageBreakdown breakdown; if (StrUtil.isBlank(feedback.getWageBreakdown())) { breakdown = wageCalculator.calculate(processService.getProcess(feedback.getProcessId()), feedback.getRouteId(), feedback.getMachineryId(), feedback.getWorkHours(), feedback.getFeedbackQuantity(), qualifiedQuantity, feedback.getId() != null ? feedbackAuxMapper.selectListByFeedbackId(feedback.getId()) : Collections.emptyList(), feedback.getFeedbackTime() != null ? feedback.getFeedbackTime().toLocalDate() : null, wageFieldValues); } else { breakdown = wageCalculator.fromSnapshot(feedback, qualifiedQuantity, wageFieldValues); } if (qualityResult != null) { wageCalculator.applyQuality(breakdown, feedback.getProcessId(), feedback.getRouteId(), qualityResult.passRate(), qualityResult.unqualifiedQuantity()); } applyWageBreakdown(feedback, breakdown); } /** * 取报工单关联的过程检验单上质检填写的工资字段值 * *

同一报工单存在多张检验单时,已完成的优先;字段未填写时不放入 Map,计算时回退工序固定值。 */ private Map getWageFieldValues(Long feedbackId) { if (feedbackId == null) { return Collections.emptyMap(); } List ipqcList = ipqcMapper.selectListBySourceDoc( MesBizTypeConstants.PRO_FEEDBACK, feedbackId); if (CollUtil.isEmpty(ipqcList)) { return Collections.emptyMap(); } List ipqcIds = new ArrayList<>(ipqcList.size()); ipqcList.stream() .filter(ipqc -> ObjUtil.equal(ipqc.getStatus(), MesQcStatusEnum.FINISHED.getStatus())) .map(MesQcIpqcDO::getId).forEach(ipqcIds::add); ipqcList.stream() .filter(ipqc -> !ObjUtil.equal(ipqc.getStatus(), MesQcStatusEnum.FINISHED.getStatus())) .map(MesQcIpqcDO::getId).forEach(ipqcIds::add); return ipqcWageFieldService.getValueMapByIpqcIds(ipqcIds); } /** * 校验质检工序的过程检验已完成,并汇总合格率与不合格数量 */ private MesProFeedbackQualityResult validateAndGetQualityResult(MesProFeedbackDO feedback) { MesProProcessDO process = processService.getProcess(feedback.getProcessId()); String processName = process != null ? process.getName() : "ID=" + feedback.getProcessId(); List finishedList = ipqcMapper.selectListBySourceDoc( MesBizTypeConstants.PRO_FEEDBACK, feedback.getId()).stream() .filter(ipqc -> ObjUtil.equal(ipqc.getStatus(), MesQcStatusEnum.FINISHED.getStatus())) .toList(); if (CollUtil.isEmpty(finishedList)) { throw new ServiceException(PRO_FEEDBACK_QUALITY_INSPECTION_NOT_FINISHED.getCode(), StrUtil.format("工序[{}]为质检工序,报工单[{}]尚未完成过程检验;" + "请到「质量管理-过程检验」完成检验后再审批", processName, feedback.getCode())); } BigDecimal checkQuantity = BigDecimal.ZERO; BigDecimal qualifiedQuantity = BigDecimal.ZERO; BigDecimal unqualifiedQuantity = BigDecimal.ZERO; for (MesQcIpqcDO ipqc : finishedList) { checkQuantity = checkQuantity.add(ObjectUtil.defaultIfNull(ipqc.getCheckQuantity(), BigDecimal.ZERO)); qualifiedQuantity = qualifiedQuantity.add(ObjectUtil.defaultIfNull(ipqc.getQualifiedQuantity(), BigDecimal.ZERO)); unqualifiedQuantity = unqualifiedQuantity.add( ObjectUtil.defaultIfNull(ipqc.getUnqualifiedQuantity(), BigDecimal.ZERO)); } if (checkQuantity.signum() <= 0) { throw new ServiceException(PRO_FEEDBACK_QUALITY_INSPECTION_NOT_FINISHED.getCode(), StrUtil.format("工序[{}]的过程检验单[{}]检测数量为 0,无法计算合格率;" + "请在「质量管理-过程检验」补录检测数量后再审批", processName, finishedList.get(0).getCode())); } BigDecimal passRate = qualifiedQuantity.multiply(BigDecimal.valueOf(100)) .divide(checkQuantity, 2, RoundingMode.HALF_UP); return new MesProFeedbackQualityResult(passRate, checkQuantity, qualifiedQuantity, unqualifiedQuantity); } /** * 顺序报工校验:同一生产工单内,上一道工序必须已报工完成,否则不允许报本道工序 * *

判定口径:上一道工序的有效任务(未取消)累计审批通过的产量已达到其排产数量, * 即任一有效任务 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 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 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 @Transactional(rollbackFor = Exception.class) public MesProFeedbackQuickCreateRespVO quickCreateFeedback(MesProFeedbackQuickCreateReqVO reqVO) { equipmentService.lock(); // 1. 校验任务存在且非终态 MesProTaskDO task = taskService.validateTaskNotFinished(reqVO.getTaskId()); // 校验停工状态不能报工 if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) { throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED); } // 1.1 顺序报工校验:同一生产工单内,上一道工序必须已报工完成,否则不允许报工 validatePreviousProcessFinished(task); // 2. 校验工艺路线工序配置 MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId( task.getRouteId(), task.getProcessId()); 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); } // 4. 构建报工DO MesProFeedbackDO feedback = new MesProFeedbackDO() .setCode(autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_FEEDBACK_CODE.getCode())) .setType(MesProFeedbackTypeEnum.SELF.getType()) .setWorkstationId(task.getWorkstationId()) .setRouteId(task.getRouteId()) .setProcessId(task.getProcessId()) .setWorkOrderId(task.getWorkOrderId()) .setTaskId(reqVO.getTaskId()) .setItemId(task.getItemId()) .setFeedbackQuantity(reqVO.getFeedbackQuantity()) .setWorkHours(reqVO.getWorkHours()) .setScheduledQuantity(task.getQuantity()) .setFeedbackTime(reqVO.getFeedbackTime() != null ? reqVO.getFeedbackTime() : LocalDateTime.now()) .setMachineryIds(reqVO.getMachineryIds()) .setMachineryId(firstMachineryId(reqVO.getMachineryIds())) .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus()) .setRemark(reqVO.getRemark()); // 4.1 快捷报工默认没有工时录入入口:计时计薪且已定价的工序,必须显式传工时; // 批量报工等不传工时的入口会得到可执行的提示,引导到报工页面填写 MesProProcessDO process = processService.getProcess(task.getProcessId()); if (process == null) { throw exception(PRO_FEEDBACK_PROCESS_NOT_EXISTS, task.getProcessId()); } BigDecimal wageRate = ObjectUtil.defaultIfNull(wageRuleService.resolve(task.getProcessId(), task.getRouteId(), feedback.getMachineryId(), process.getBillingType(), LocalDate.now()).getWageRate(), BigDecimal.ZERO); if (MesProProcessBillingTypeEnum.TIME == MesProProcessBillingTypeEnum.valueOfType(process.getBillingType()) && wageRate.signum() > 0 && reqVO.getWorkHours() == null) { throw exception(PRO_FEEDBACK_TIME_BILLING_QUICK_UNSUPPORTED, process.getName()); } // 4.2 快捷报工没有辅助项入口,仅按生产长度(报工单位为米时)计算标准工时快照 List auxList = worktimeService.calculateForFeedback(feedback, null); // 4.3 按工序计费类型、单价规则与工资字段快照并计算本次报工工资 applyProcessBilling(feedback, process, auxList); equipmentService.validateSubmission(feedback); feedbackMapper.insert(feedback); saveFeedbackAux(feedback.getId(), auxList); 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()) && !ObjUtil.equal(fb.getStatus(), MesProFeedbackStatusEnum.UNCHECK.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) .setReportableQuantity(reportableQuantity); } @Override public MesProFeedbackWageBreakdown previewWage(MesProFeedbackWagePreviewReqVO reqVO) { MesProTaskDO task = taskService.getTask(reqVO.getTaskId()); if (task == null) { throw exception(PRO_TASK_NOT_EXISTS); } MesProProcessDO process = processService.getProcess(task.getProcessId()); if (process == null) { throw exception(PRO_FEEDBACK_PROCESS_NOT_EXISTS, task.getProcessId()); } // 借用一个临时报工单对象复用标准工时/辅助工时计算 MesProFeedbackDO temp = new MesProFeedbackDO() .setTaskId(task.getId()) .setRouteId(task.getRouteId()) .setProcessId(task.getProcessId()) .setWorkstationId(task.getWorkstationId()) .setMachineryId(reqVO.getMachineryId()) .setWorkHours(reqVO.getWorkHours()) .setFeedbackQuantity(reqVO.getFeedbackQuantity()) .setQualifiedQuantity(reqVO.getQualifiedQuantity()) .setProductionLength(reqVO.getProductionLength()); List auxList = worktimeService.calculateForFeedback(temp, reqVO.getAuxList()); return wageCalculator.calculate(process, task.getRouteId(), reqVO.getMachineryId(), reqVO.getWorkHours(), reqVO.getFeedbackQuantity(), reqVO.getQualifiedQuantity(), auxList, reqVO.getFeedbackDate()); } @Override @Transactional(rollbackFor = Exception.class) public MesProFeedbackBatchCreateRespVO batchCreateFeedback(MesProFeedbackBatchCreateReqVO reqVO) { List results = new java.util.ArrayList<>(); int successCount = 0; int failCount = 0; for (Long taskId : reqVO.getTaskIds()) { try { MesProFeedbackQuickCreateReqVO singleReq = new MesProFeedbackQuickCreateReqVO(); singleReq.setTaskId(taskId); singleReq.setFeedbackQuantity(reqVO.getFeedbackQuantity()); singleReq.setFeedbackTime(reqVO.getFeedbackTime()); singleReq.setRemark(reqVO.getRemark()); MesProFeedbackQuickCreateRespVO resp = quickCreateFeedback(singleReq); results.add(new MesProFeedbackBatchCreateRespVO.BatchResult() .setTaskId(taskId) .setSuccess(true) .setFeedbackId(resp.getFeedbackId())); successCount++; } catch (Exception e) { results.add(new MesProFeedbackBatchCreateRespVO.BatchResult() .setTaskId(taskId) .setSuccess(false) .setErrorMsg(e.getMessage())); failCount++; } } return new MesProFeedbackBatchCreateRespVO() .setSuccessCount(successCount) .setFailCount(failCount) .setResults(results); } @Override @Transactional(rollbackFor = Exception.class) public void updateProFeedbackWhenIpqcFinish(Long feedbackId, Long sourceLineId, BigDecimal qualifiedQty, BigDecimal unqualifiedQty, BigDecimal laborScrapQty, BigDecimal materialScrapQty, BigDecimal otherScrapQty) { // 1. 校验报工单存在且为待检验状态 MesProFeedbackDO feedback = feedbackMapper.selectByIdForUpdate(feedbackId); if (feedback == null) { throw exception(PRO_FEEDBACK_NOT_EXISTS); } if (ObjUtil.notEqual(feedback.getStatus(), MesProFeedbackStatusEnum.UNCHECK.getStatus())) { throw exception(PRO_FEEDBACK_NOT_UNCHECK); } // 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())); // 4. 产量已在报工审批时累计,此处只回填质量数量。 feedback.setQualifiedQuantity(qualifiedQty).setUnqualifiedQuantity(unqualifiedQty); taskService.updateQualityQuantity(feedback.getTaskId(), ObjectUtil.defaultIfNull(qualifiedQty, BigDecimal.ZERO), ObjectUtil.defaultIfNull(unqualifiedQty, BigDecimal.ZERO)); //5.检查并发布生产完成事件 checkAndPublishProductionFinished(feedback.getWorkOrderId()); checkAndAutoFinishAfterLastQc(feedback); } void checkAndAutoFinishAfterLastQc(MesProFeedbackDO feedback) { MesProWorkOrderProcessDO lastProcess = workOrderProcessService .getLastWorkOrderProcessByWorkOrderId(feedback.getWorkOrderId()); if (lastProcess == null || !ObjUtil.equal(lastProcess.getProcessId(), feedback.getProcessId())) { return; } // 同一工单的末道报工全部完成质检后,才允许级联完成工单任务。 Long pendingCount = feedbackMapper.selectCount(new LambdaQueryWrapperX() .eq(MesProFeedbackDO::getWorkOrderId, feedback.getWorkOrderId()) .eq(MesProFeedbackDO::getProcessId, feedback.getProcessId()) .ne(MesProFeedbackDO::getStatus, MesProFeedbackStatusEnum.FINISHED.getStatus())); if (pendingCount == 0) { workOrderService.checkAndAutoFinishWorkOrder(feedback.getWorkOrderId()); } } @Override public MesProFeedbackBatchCheckRespVO checkBatchRequirements(Long feedbackId) { MesProFeedbackDO feedback = validateFeedbackExists(feedbackId); MesProFeedbackBatchCheckRespVO respVO = new MesProFeedbackBatchCheckRespVO(); // 1. 检查物料是否启用批次管理 MesMdmItemDO item = mdmItemService.getItem(feedback.getItemId()); if (item == null || Boolean.FALSE.equals(item.getIsBatchManaged())) { respVO.setNeedBatchInput(false); respVO.setIsBatchManaged(false); return respVO; } respVO.setIsBatchManaged(true); // 2. 获取批次配置 MdmItemBatchConfigRespDTO config = itemBatchConfigApi.getItemBatchConfig(feedback.getItemId()); if (config == null) { respVO.setNeedBatchInput(false); return respVO; } // 3. 填充配置标志 respVO.setProduceDateFlag(config.getProduceDateFlag()); respVO.setExpireDateFlag(config.getExpireDateFlag()); respVO.setReceiptDateFlag(config.getReceiptDateFlag()); respVO.setVendorFlag(config.getVendorFlag()); respVO.setClientFlag(config.getClientFlag()); respVO.setSalesOrderCodeFlag(config.getSalesOrderCodeFlag()); respVO.setPurchaseOrderCodeFlag(config.getPurchaseOrderCodeFlag()); respVO.setWorkOrderFlag(config.getWorkOrderFlag()); respVO.setTaskFlag(config.getTaskFlag()); respVO.setWorkstationFlag(config.getWorkstationFlag()); respVO.setToolFlag(config.getToolFlag()); respVO.setMoldFlag(config.getMoldFlag()); respVO.setLotNumberFlag(config.getLotNumberFlag()); respVO.setQualityStatusFlag(config.getQualityStatusFlag()); // 4. 判断哪些字段已自动填充(报工和生产过程中必然有值的字段) List autoFilled = new ArrayList<>(); // 生产日期:审批时自动取当天 autoFilled.add("produceDate"); // 生产工单:从报工记录获取 autoFilled.add("workOrderId"); // 生产任务:从报工记录获取 autoFilled.add("taskId"); // 工作站:从报工记录获取 autoFilled.add("workstationId"); respVO.setAutoFilledFields(autoFilled); // 5. 判断是否需要弹窗:存在必填但无法自动填充的字段 boolean needBatchInput = false; if (Boolean.TRUE.equals(config.getExpireDateFlag())) needBatchInput = true; if (Boolean.TRUE.equals(config.getReceiptDateFlag())) needBatchInput = true; if (Boolean.TRUE.equals(config.getVendorFlag())) needBatchInput = true; if (Boolean.TRUE.equals(config.getClientFlag())) needBatchInput = true; if (Boolean.TRUE.equals(config.getSalesOrderCodeFlag())) needBatchInput = true; if (Boolean.TRUE.equals(config.getPurchaseOrderCodeFlag())) needBatchInput = true; if (Boolean.TRUE.equals(config.getToolFlag())) needBatchInput = true; if (Boolean.TRUE.equals(config.getMoldFlag())) needBatchInput = true; if (Boolean.TRUE.equals(config.getLotNumberFlag())) needBatchInput = true; if (Boolean.TRUE.equals(config.getQualityStatusFlag())) needBatchInput = true; respVO.setNeedBatchInput(needBatchInput); return respVO; } }