| | |
| | | package cn.iocoder.yudao.module.mes.service.pro.feedback; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | 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 cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackPageReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedbackSaveReqVO; |
| | | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | | 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; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO; |
| | | 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.mysql.pro.feedback.MesProFeedbackMapper; |
| | | 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.md.workstation.MesMdWorkstationService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService; |
| | | import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductBomService; |
| | | 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.wm.itemconsume.MesWmItemConsumeService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.productissue.MesWmProductIssueService; |
| | | 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.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.util.ArrayList; |
| | | import java.util.Collection; |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.Set; |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | import java.util.stream.Collectors; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; |
| | | import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; |
| | | |
| | | /** |
| | | * MES 生产报工 Service 实现类 |
| | | * |
| | | * @author 芋道源码 |
| | | * @author 超级管理员 |
| | | */ |
| | | @Service |
| | | @Validated |
| | |
| | | |
| | | @Resource |
| | | private MesProFeedbackMapper feedbackMapper; |
| | | @Resource |
| | | private MesFeedbackParamService feedbackParamService; |
| | | |
| | | @Resource |
| | | private MesProWorkOrderService workOrderService; |
| | | @Resource |
| | | private MesProRouteProcessService routeProcessService; |
| | | @Resource |
| | | private MesProRouteProductBomService routeProductBomService; |
| | | private MesProWorkOrderBomService workOrderBomService; |
| | | @Resource |
| | | private MesMdWorkstationService workstationService; |
| | | private MesProWorkOrderProcessService workOrderProcessService; |
| | | @Resource |
| | | @Lazy // 避免循环依赖 |
| | | private MesProTaskService taskService; |
| | | @Resource |
| | | private MesWmItemConsumeService itemConsumeService; |
| | | @Resource |
| | | private MesWmProductIssueService productIssueService; |
| | | @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 MesQcTemplateItemService templateItemService; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long createFeedback(MesProFeedbackSaveReqVO createReqVO) { |
| | | // 1. 校验 |
| | | MesProTaskDO task = validateFeedbackData(createReqVO); |
| | | task = feedbackParamService.lockTask(task.getId()); |
| | | validateLockedTaskDimensions(createReqVO, task); |
| | | |
| | | // 2. 插入(自动填充 itemId) |
| | | // 2. 插入(主键由数据库自增,忽略请求中可能残留的 id;自动填充 itemId) |
| | | MesProFeedbackDO feedback = BeanUtils.toBean(createReqVO, MesProFeedbackDO.class) |
| | | .setId(null) |
| | | .setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus()) |
| | | .setItemId(task.getItemId()); |
| | | .setItemId(task.getItemId()) |
| | | .setParamSnapshot(feedbackParamService.buildDraft(task, createReqVO.getParamValues())); |
| | | feedbackMapper.insert(feedback); |
| | | return feedback.getId(); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void updateFeedback(MesProFeedbackSaveReqVO updateReqVO) { |
| | | // 1.1 校验存在 + 草稿状态 |
| | | validateFeedbackStatusPrepare(updateReqVO.getId()); |
| | | // 先锁报工再锁任务,与提交顺序一致。 |
| | | MesProFeedbackDO original = feedbackMapper.selectOne(new LambdaQueryWrapperX<MesProFeedbackDO>() |
| | | .eq(MesProFeedbackDO::getId, updateReqVO.getId()).last("FOR UPDATE")); |
| | | if (original == null) throw exception(PRO_FEEDBACK_NOT_EXISTS); |
| | | if (!Objects.equals(original.getStatus(), MesProFeedbackStatusEnum.PREPARE.getStatus())) |
| | | throw exception(PRO_FEEDBACK_NOT_PREPARE); |
| | | // 1.2 校验业务数据 |
| | | MesProTaskDO task = validateFeedbackData(updateReqVO); |
| | | task = feedbackParamService.lockTask(task.getId()); |
| | | validateLockedTaskDimensions(updateReqVO, task); |
| | | |
| | | // 2. 更新(自动填充 itemId) |
| | | MesProFeedbackDO updateObj = BeanUtils.toBean(updateReqVO, MesProFeedbackDO.class) |
| | | .setItemId(task.getItemId()); |
| | | if (updateReqVO.getParamValues() != null) { |
| | | String snapshot = feedbackParamService.buildDraft(task, updateReqVO.getParamValues()); |
| | | updateObj.setParamSnapshot(snapshot == null ? "" : snapshot); |
| | | } else if (!Objects.equals(original.getTaskId(), task.getId())) { |
| | | updateObj.setParamSnapshot(""); |
| | | } |
| | | feedbackMapper.updateById(updateObj); |
| | | } |
| | | |
| | | @Override |
| | | public void deleteFeedback(Long id) { |
| | | // 1. 校验存在 + 草稿状态 |
| | | validateFeedbackStatusPrepare(id); |
| | | private void validateLockedTaskDimensions(MesProFeedbackSaveReqVO req, MesProTaskDO task) { |
| | | if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED); |
| | | if (!Objects.equals(req.getProcessId(), task.getProcessId()) |
| | | || !Objects.equals(req.getWorkOrderId(), task.getWorkOrderId()) |
| | | || !Objects.equals(req.getRouteId(), task.getRouteId())) |
| | | throw exception(PRO_FEEDBACK_PARAM_INVALID, "任务信息已变更,请重新选择任务"); |
| | | } |
| | | |
| | | // 2. 删除 |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public MesProFeedbackImportRespVO importFeedbackList(List<MesProFeedbackImportExcelVO> importList, |
| | | boolean updateSupport) { |
| | | // 1. 校验非空 |
| | | if (CollUtil.isEmpty(importList)) { |
| | | throw exception(MES_PRO_FEEDBACK_IMPORT_LIST_IS_EMPTY); |
| | | } |
| | | |
| | | // 2. 逐行处理 |
| | | MesProFeedbackImportRespVO respVO = MesProFeedbackImportRespVO.builder() |
| | | .createCodes(new ArrayList<>()).updateCodes(new ArrayList<>()) |
| | | .failureCodes(new LinkedHashMap<>()).build(); |
| | | AtomicInteger index = new AtomicInteger(1); |
| | | importList.forEach(importItem -> { |
| | | int currentIndex = index.getAndIncrement(); |
| | | String key = StrUtil.blankToDefault(importItem.getCode(), "第 " + currentIndex + " 行"); |
| | | if (StrUtil.isBlank(importItem.getCode())) { |
| | | respVO.getFailureCodes().put(key, "报工单编号不能为空"); |
| | | return; |
| | | } |
| | | if (StrUtil.isBlank(importItem.getTaskCode())) { |
| | | respVO.getFailureCodes().put(key, "生产任务编码不能为空"); |
| | | return; |
| | | } |
| | | // 2.1 解析生产任务 |
| | | MesProTaskDO task = taskService.getTaskByCode(importItem.getTaskCode().trim()); |
| | | if (task == null) { |
| | | respVO.getFailureCodes().put(key, "生产任务编码[" + importItem.getTaskCode() + "]不存在"); |
| | | return; |
| | | } |
| | | // 2.2 构建请求并创建/更新(复用 create/update 服务,报工人/审核人取当前登录用户) |
| | | MesProFeedbackSaveReqVO reqVO = BeanUtils.toBean(importItem, MesProFeedbackSaveReqVO.class); |
| | | reqVO.setTaskId(task.getId()); |
| | | Long loginUserId = getLoginUserId(); |
| | | reqVO.setFeedbackUserId(ObjUtil.defaultIfNull(reqVO.getFeedbackUserId(), loginUserId)); |
| | | reqVO.setApproveUserId(ObjUtil.defaultIfNull(reqVO.getApproveUserId(), loginUserId)); |
| | | MesProFeedbackDO existFeedback = feedbackMapper.selectByCode(importItem.getCode()); |
| | | try { |
| | | if (existFeedback == null) { |
| | | reqVO.setId(null); |
| | | createFeedback(reqVO); |
| | | respVO.getCreateCodes().add(importItem.getCode()); |
| | | } else if (updateSupport) { |
| | | reqVO.setId(existFeedback.getId()); |
| | | updateFeedback(reqVO); |
| | | respVO.getUpdateCodes().add(importItem.getCode()); |
| | | } else { |
| | | respVO.getFailureCodes().put(key, "报工单编号已存在"); |
| | | } |
| | | } catch (ServiceException e) { |
| | | respVO.getFailureCodes().put(key, e.getMessage()); |
| | | } |
| | | }); |
| | | return respVO; |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void deleteFeedback(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 (!Objects.equals(feedback.getStatus(), MesProFeedbackStatusEnum.PREPARE.getStatus())) |
| | | throw exception(PRO_FEEDBACK_NOT_PREPARE); |
| | | feedbackMapper.deleteById(id); |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void submitFeedback(Long id) { |
| | | // 1. 校验存在 + 草稿状态 |
| | | validateFeedbackStatusPrepare(id); |
| | | |
| | | // 2. 更新状态为审批中(报工人和报工时间由表单保存时确定,提交不覆盖) |
| | | feedbackMapper.updateById(new MesProFeedbackDO().setId(id) |
| | | .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus())); |
| | | MesProFeedbackDO feedback = feedbackMapper.selectOne(new LambdaQueryWrapperX<MesProFeedbackDO>() |
| | | .eq(MesProFeedbackDO::getId, id).last("FOR UPDATE")); |
| | | if (feedback == null) throw exception(PRO_FEEDBACK_NOT_EXISTS); |
| | | 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()); |
| | | if (snapshot != null) update.setParamSnapshot(snapshot); |
| | | feedbackMapper.updateById(update); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void rejectFeedback(Long id) { |
| | | // 1. 校验存在 + 审批中状态 |
| | | validateFeedbackStatusApproving(id); |
| | | MesProFeedbackDO feedback = lockFeedbackStatusApproving(id); |
| | | // 1.1 校验当前登录人是该报工单的指定审核人 |
| | | validateCurrentUserIsApprover(feedback); |
| | | |
| | | // 2. 更新状态为草稿 |
| | | feedbackMapper.updateById(new MesProFeedbackDO().setId(id) |
| | |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public boolean approveFeedback(Long id) { |
| | | public boolean approveFeedback(MesProFeedbackApproveReqVO reqVO) { |
| | | // 1.1 校验存在 + 审批中状态 |
| | | MesProFeedbackDO feedback = validateFeedbackStatusApproving(id); |
| | | MesProFeedbackDO feedback = lockFeedbackStatusApproving(reqVO.getId()); |
| | | // 1.1.1 校验当前登录人是该报工单的指定审核人 |
| | | validateCurrentUserIsApprover(feedback); |
| | | // 1.2 校验报工数量 > 0 |
| | | if (feedback.getFeedbackQuantity() == null |
| | | || feedback.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) { |
| | |
| | | // 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 |
| | |
| | | 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<MesProRouteProductBomDO> boms = routeProductBomService.getRouteProductBomList( |
| | | feedback.getRouteId(), feedback.getProcessId(), feedback.getItemId()); |
| | | // 3.1 校验当前倒冲工序必须配置投料 BOM,避免静默不扣料 |
| | | List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderIdAndProcessId( |
| | | feedback.getWorkOrderId(), feedback.getProcessId()); |
| | | if (CollUtil.isEmpty(boms)) { |
| | | throw exception(PRO_FEEDBACK_BACKFLUSH_BOM_REQUIRED); |
| | | } |
| | |
| | | MesWmItemConsumeDO itemConsume = itemConsumeService.generateItemConsume(feedback); |
| | | if (itemConsume != null) { |
| | | itemConsumeService.finishItemConsume(itemConsume.getId()); |
| | | // 消耗单负责实际扣库;领料单仅作为倒冲业务凭证,避免重复扣减库存 |
| | | productIssueService.generateBackflushProductIssue(feedback, itemConsume); |
| | | } |
| | | } |
| | | |
| | | // 4. 关键工序:生成产出单,并根据是否需要检验决定入库方式 |
| | | if (keyFlag) { |
| | | // 4.1 需要检验:生成产出单(质量状态=待检验),更新报工状态为待检验,等质检完成回调后再入库 |
| | | if (checkFlag) { |
| | | // 完成时回调见:MesQcIpqcServiceImpl#finishIpqc → splitPendingAndFinishProduce + completeFeedbackFromIpqc |
| | | productProduceService.generateProductProduce(feedback, true); |
| | | feedbackMapper.updateById(new MesProFeedbackDO().setId(id) |
| | | .setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus())); |
| | | return false; |
| | | } |
| | | // 4.2 无需检验:生成产出单(按合格/不合格拆行),直接完成入库,并更新任务/工单的已生产数量 |
| | | MesWmProductProduceDO produce = productProduceService.generateProductProduce(feedback, false); |
| | | // 4. 先生成产出;进度仅在产出完成且报工状态变为完成后更新。 |
| | | if (checkFlag) { |
| | | // 6.1 需要检验:生成产出单(质量状态=待检验),更新报工状态为待检验,等质检完成回调后再入库 |
| | | // 完成时回调见:MesQcIpqcServiceImpl#finishIpqc → splitPendingAndFinishProduce + updateProFeedbackWhenIpqcFinish |
| | | productProduceService.generateProductProduce(feedback, true, reqVO); |
| | | feedbackMapper.updateById(new MesProFeedbackDO().setId(reqVO.getId()) |
| | | .setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus())); |
| | | return false; |
| | | } else { |
| | | // 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); |
| | | |
| | | } |
| | | |
| | | // 5. 非关键工序 / 关键非质检工序:直接完成(清零 uncheckQuantity 防止 !key+check 留脏数据) |
| | | feedbackMapper.updateById(new MesProFeedbackDO().setId(id) |
| | | .setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus()) |
| | | .setUncheckQuantity(BigDecimal.ZERO)); |
| | | return true; // 已完成 |
| | | } |
| | | |
| | |
| | | * <li>使用报工数量(feedbackQuantity),累加任务和工单的已生产数量</li> |
| | | * <li>使用产出单行按 qualityStatus 聚合的合格品/不合格品数量,累加任务的合格品/不合格品数量 |
| | | * (不直接用 feedback 上的数量,确保质检回调场景下数量来源正确)</li> |
| | | * isPush 是否推送生产完成事件 |
| | | * </ul> |
| | | * |
| | | * @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()); |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | // 2. 更新任务的已生产/合格/不合格数量 |
| | | // 2. 更新任务的已生产/合格/不合格数量 |
| | | taskService.updateProducedQuantity(feedback.getTaskId(), |
| | | feedback.getFeedbackQuantity(), qualifiedQty, unqualifiedQty); |
| | | // 3. 更新工单的已生产数量 |
| | | workOrderService.updateProducedQuantity(feedback.getWorkOrderId(), |
| | | feedback.getFeedbackQuantity()); |
| | | // 3. 更新订单的已生产数量 |
| | | // 新增逻辑:只有在该笔订单最后一个工序才更新生产订单的已生产数量 |
| | | MesProWorkOrderProcessDO lastProcess = workOrderProcessService.getLastWorkOrderProcessByWorkOrderId(feedback.getWorkOrderId()); |
| | | 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 的已生产数量(如果是销售订单来源) |
| | | MesProMpsRespDTO mps = mpsApi.getMpsByWorkOrderId(feedback.getWorkOrderId()); |
| | | if (mps != null) { |
| | | mpsApi.updateMpsProducedQuantity(mps.getId(), feedback.getFeedbackQuantity()); |
| | | } |
| | | } |
| | | |
| | | |
| | | // 5. 检查并发布生产完成事件 |
| | | checkAndPublishProductionFinished(feedback.getWorkOrderId()); |
| | | } |
| | | |
| | | /** |
| | | * 检查并发布生产完成事件 |
| | | */ |
| | | private void checkAndPublishProductionFinished(Long workOrderId) { |
| | | // 1. 获取工单关联的 MPS |
| | | MesProMpsRespDTO mps = mpsApi.getMpsByWorkOrderId(workOrderId); |
| | | if (mps == null || mps.getSaleOrderId() == null) { |
| | | return; // 非销售订单来源的工单,跳过 |
| | | } |
| | | |
| | | // 2. 检查并自动完成 MPS(如果满足条件) |
| | | mpsApi.checkAndFinishMps(mps.getId()); |
| | | |
| | | // 3. 检查销售订单所有生产是否完成 |
| | | if (mpsApi.checkSaleOrderProductionFinished(mps.getSaleOrderId())) { |
| | | eventPublisher.publishEvent(new ProductionFinishedEvent(mps.getSaleOrderId(), java.time.LocalDateTime.now())); |
| | | } |
| | | } |
| | | |
| | | // ==================== 校验方法 ==================== |
| | |
| | | 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())) { |
| | |
| | | } |
| | | |
| | | /** |
| | | * 校验当前登录用户是否为报工单指定的审核人 |
| | | * |
| | | * 报工单创建时必须指定 approveUserId,审批通过/驳回只能由该审核人操作, |
| | | * 避免任意拥有菜单权限的用户越权审批。 |
| | | * |
| | | * @param feedback 报工单 |
| | | */ |
| | | private void validateCurrentUserIsApprover(MesProFeedbackDO feedback) { |
| | | if (feedback.getApproveUserId() == null) { |
| | | throw exception(PRO_FEEDBACK_APPROVER_NOT_CONFIGURED); |
| | | } |
| | | if (!feedback.getApproveUserId().equals(getLoginUserId())) { |
| | | throw exception(PRO_FEEDBACK_NOT_APPROVER); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 校验当前登录人是否为任务的报工人;任务未配置报工人时无人可报工 |
| | | */ |
| | | 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 |
| | | * 从任务自动填充,无需前端重复传入。 |
| | | * |
| | | * @param reqVO 报工请求 |
| | | * @return 关联的生产任务 |
| | | */ |
| | | private MesProTaskDO validateFeedbackData(MesProFeedbackSaveReqVO reqVO) { |
| | | // 1. 校验工作站存在 |
| | | MesMdWorkstationDO workstation = workstationService.validateWorkstationExists(reqVO.getWorkstationId()); |
| | | if (ObjUtil.notEqual(workstation.getProcessId(), reqVO.getProcessId())) { |
| | | throw exception(PRO_WORKSTATION_PROCESS_MISMATCH); |
| | | } |
| | | // 1. 校验任务存在且未终态 |
| | | MesProTaskDO task = taskService.validateTaskNotFinished(reqVO.getTaskId()); |
| | | |
| | | // 2.1 校验工艺路线 + 工序配置有效 |
| | | MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId( |
| | | reqVO.getRouteId(), reqVO.getProcessId()); |
| | | if (routeProcess == null) { |
| | | throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID); |
| | | // 校验停工状态不能报工 |
| | | if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) { |
| | | throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED); |
| | | } |
| | | // 2.2 校验数量 |
| | | boolean checkFlag = Boolean.TRUE.equals(routeProcess.getCheckFlag()); |
| | | // 校验当前登录人是该任务的报工人 |
| | | validateTaskAssignee(task); |
| | | |
| | | // 2. 从任务自动填充关联字段(前端无需传入,后端统一从任务获取) |
| | | reqVO.setWorkOrderId(task.getWorkOrderId()); |
| | | reqVO.setRouteId(task.getRouteId()); |
| | | reqVO.setProcessId(task.getProcessId()); |
| | | reqVO.setItemId(task.getItemId()); |
| | | reqVO.setWorkstationId(task.getWorkstationId()); |
| | | |
| | | // 3. 校验工单工序 / 工艺路线工序配置有效 |
| | | boolean checkFlag = resolveProcessFlags(task.getRouteId(), task.getWorkOrderId(), task.getProcessId()).checkFlag(); |
| | | // 4.1 校验数量 |
| | | if (checkFlag) { |
| | | // 需要检验:只需填报工数量,且必须 > 0 |
| | | if (reqVO.getFeedbackQuantity() == null |
| | |
| | | } |
| | | } |
| | | |
| | | // 3. 校验工单已确认 |
| | | MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId()); |
| | | if (ObjUtil.notEqual(workOrder.getProductId(), reqVO.getItemId())) { |
| | | throw exception(PRO_WORK_ORDER_PRODUCT_MISMATCH); |
| | | } |
| | | |
| | | // 4. 校验任务存在且未终态(已完成/已取消),并返回任务用于冗余 itemId |
| | | MesProTaskDO task = taskService.validateTaskNotFinished(reqVO.getTaskId()); |
| | | validateTaskRelation(task, workstation, workOrder, reqVO); |
| | | return task; |
| | | } |
| | | |
| | | private void validateTaskRelation(MesProTaskDO task, MesMdWorkstationDO workstation, |
| | | MesProWorkOrderDO workOrder, MesProFeedbackSaveReqVO reqVO) { |
| | | if (ObjUtil.notEqual(task.getWorkOrderId(), workOrder.getId())) { |
| | | throw exception(PRO_TASK_WORK_ORDER_MISMATCH); |
| | | /** |
| | | * 解析工序的检验标识与倒冲标识 |
| | | * |
| | | * <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); |
| | | } |
| | | if (ObjUtil.notEqual(task.getWorkstationId(), workstation.getId())) { |
| | | throw exception(PRO_TASK_WORKSTATION_MISMATCH); |
| | | 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) { |
| | | // 1. 校验任务存在且非终态 |
| | | MesProTaskDO task = taskService.validateTaskNotFinished(reqVO.getTaskId()); |
| | | |
| | | // 校验停工状态不能报工 |
| | | if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) { |
| | | throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED); |
| | | } |
| | | if (ObjUtil.notEqual(task.getRouteId(), reqVO.getRouteId()) |
| | | || ObjUtil.notEqual(task.getProcessId(), reqVO.getProcessId())) { |
| | | throw exception(PRO_TASK_ROUTE_PROCESS_MISMATCH); |
| | | // 校验当前登录人是该任务的报工人 |
| | | validateTaskAssignee(task); |
| | | |
| | | // 2. 校验工单工序 / 工艺路线工序配置 |
| | | boolean checkFlag = resolveProcessFlags(task.getRouteId(), task.getWorkOrderId(), task.getProcessId()).checkFlag(); |
| | | // 3. 校验数量 |
| | | 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); |
| | | } |
| | | } |
| | | if (ObjUtil.notEqual(task.getItemId(), reqVO.getItemId())) { |
| | | throw exception(PRO_TASK_ITEM_MISMATCH); |
| | | // 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()) |
| | | .setQualifiedQuantity(reqVO.getQualifiedQuantity()) |
| | | .setUnqualifiedQuantity(reqVO.getUnqualifiedQuantity()) |
| | | .setScheduledQuantity(task.getQuantity()) |
| | | .setFeedbackTime(LocalDateTime.now()) |
| | | .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus()) |
| | | .setRemark(reqVO.getRemark()); |
| | | MesProTaskDO lockedTask = feedbackParamService.lockTask(task.getId()); |
| | | if (!MesProTaskStatusEnum.canFeedback(lockedTask.getStatus())) throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED); |
| | | if (!Objects.equals(task.getProcessId(), lockedTask.getProcessId()) |
| | | || !Objects.equals(task.getWorkOrderId(), lockedTask.getWorkOrderId()) |
| | | || !Objects.equals(task.getRouteId(), lockedTask.getRouteId())) |
| | | throw exception(PRO_FEEDBACK_PARAM_INVALID, "任务信息已变更,请重新报工"); |
| | | String draftParam = feedbackParamService.buildDraft(lockedTask, reqVO.getParamValues()); |
| | | feedback.setParamSnapshot(feedbackParamService.complete(lockedTask, draftParam)); |
| | | feedbackMapper.insert(feedback); |
| | | // 5. 返回结果 |
| | | MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(task.getWorkOrderId()); |
| | | return new MesProFeedbackQuickCreateRespVO() |
| | | .setFeedbackId(feedback.getId()) |
| | | .setTaskStatus(task.getStatus()) |
| | | .setWorkOrderStatus(workOrder != null ? workOrder.getStatus() : null); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public MesProFeedbackBatchCreateRespVO batchCreateFeedback(MesProFeedbackBatchCreateReqVO reqVO) { |
| | | List<MesProFeedbackBatchCreateRespVO.BatchResult> results = new java.util.ArrayList<>(); |
| | | int successCount = 0; |
| | | int failCount = 0; |
| | | for (Long taskId : reqVO.getTaskIds()) { |
| | | try { |
| | | MesProTaskDO batchTask = feedbackParamService.lockTask(taskId); |
| | | if (Objects.equals(batchTask.getParamCollectMode(), 1) |
| | | || (Objects.equals(batchTask.getParamCollectMode(), 2) && StrUtil.isBlank(batchTask.getParamSharedSnapshot()))) { |
| | | throw exception(PRO_FEEDBACK_PARAM_INVALID, "该任务需要单独填写参数,请使用单笔报工"); |
| | | } |
| | | 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() |
| | | .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 |
| | |
| | | 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); |
| | | updateTaskAndWorkOrderByFeedback(feedback); |
| | | // 4. 全部检验完成后回写任务/工单进度 |
| | | if (allInspected) { |
| | | updateTaskAndWorkOrderByFeedback(feedback); |
| | | } |
| | | } |
| | | |
| | | @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<String> 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; |
| | | } |
| | | |
| | | @Override |
| | | public MesProFeedbackSummaryRespVO getFeedbackSummary(LocalDate beginDate, LocalDate endDate, |
| | | Integer workOrderType) { |
| | | // 1. 查询有效报工(已批准:待检验 + 已完成) |
| | | LambdaQueryWrapperX<MesProFeedbackDO> query = new LambdaQueryWrapperX<MesProFeedbackDO>() |
| | | .in(MesProFeedbackDO::getStatus, List.of( |
| | | MesProFeedbackStatusEnum.UNCHECK.getStatus(), |
| | | MesProFeedbackStatusEnum.FINISHED.getStatus())) |
| | | .betweenIfPresent(MesProFeedbackDO::getFeedbackTime, |
| | | beginDate != null ? beginDate.atStartOfDay() : null, |
| | | endDate != null ? endDate.plusDays(1).atStartOfDay() : null); |
| | | List<MesProFeedbackDO> feedbackList = feedbackMapper.selectList(query); |
| | | if (CollUtil.isEmpty(feedbackList)) { |
| | | MesProFeedbackSummaryRespVO empty = new MesProFeedbackSummaryRespVO(); |
| | | empty.setTotalFeedbackQuantity(BigDecimal.ZERO); |
| | | empty.setTotalQualifiedQuantity(BigDecimal.ZERO); |
| | | empty.setTotalFeedbackCount(0); |
| | | empty.setByType(Collections.emptyList()); |
| | | empty.setByDate(Collections.emptyList()); |
| | | return empty; |
| | | } |
| | | |
| | | // 2. 加载工单信息,映射 工单ID -> 工单类型 |
| | | Set<Long> workOrderIds = feedbackList.stream() |
| | | .map(MesProFeedbackDO::getWorkOrderId).filter(Objects::nonNull) |
| | | .collect(Collectors.toSet()); |
| | | Map<Long, MesProWorkOrderDO> workOrderMap = workOrderService.getWorkOrderMap(workOrderIds); |
| | | |
| | | // 3. 过滤工单类型 |
| | | List<MesProFeedbackDO> filteredList = feedbackList.stream() |
| | | .filter(feedback -> { |
| | | if (workOrderType == null) { |
| | | return true; |
| | | } |
| | | MesProWorkOrderDO workOrder = workOrderMap.get(feedback.getWorkOrderId()); |
| | | return workOrder != null && Objects.equals(workOrder.getType(), workOrderType); |
| | | }) |
| | | .collect(Collectors.toList()); |
| | | |
| | | // 4. 汇总 |
| | | BigDecimal totalFeedbackQuantity = BigDecimal.ZERO; |
| | | BigDecimal totalQualifiedQuantity = BigDecimal.ZERO; |
| | | int totalCount = 0; |
| | | for (MesProFeedbackDO feedback : filteredList) { |
| | | totalFeedbackQuantity = totalFeedbackQuantity.add(nullToZero(feedback.getFeedbackQuantity())); |
| | | totalQualifiedQuantity = totalQualifiedQuantity.add(nullToZero(feedback.getQualifiedQuantity())); |
| | | totalCount++; |
| | | } |
| | | |
| | | // 5. 按工单类型分组 |
| | | Map<Integer, MesProFeedbackSummaryRespVO.TypeSummaryItem> typeMap = new LinkedHashMap<>(); |
| | | // 6. 按日期分组 |
| | | Map<LocalDate, MesProFeedbackSummaryRespVO.DateSummaryItem> dateMap = new LinkedHashMap<>(); |
| | | for (MesProFeedbackDO feedback : filteredList) { |
| | | MesProWorkOrderDO workOrder = workOrderMap.get(feedback.getWorkOrderId()); |
| | | Integer type = workOrder != null ? workOrder.getType() : null; |
| | | if (type != null) { |
| | | MesProFeedbackSummaryRespVO.TypeSummaryItem item = typeMap.computeIfAbsent(type, key -> { |
| | | MesProFeedbackSummaryRespVO.TypeSummaryItem t = new MesProFeedbackSummaryRespVO.TypeSummaryItem(); |
| | | t.setWorkOrderType(key); |
| | | t.setWorkOrderTypeName(MesProWorkOrderTypeEnum.valueOf(key) != null |
| | | ? MesProWorkOrderTypeEnum.valueOf(key).getName() : String.valueOf(key)); |
| | | t.setFeedbackQuantity(BigDecimal.ZERO); |
| | | t.setQualifiedQuantity(BigDecimal.ZERO); |
| | | t.setFeedbackCount(0); |
| | | return t; |
| | | }); |
| | | item.setFeedbackQuantity(item.getFeedbackQuantity().add(nullToZero(feedback.getFeedbackQuantity()))); |
| | | item.setQualifiedQuantity(item.getQualifiedQuantity().add(nullToZero(feedback.getQualifiedQuantity()))); |
| | | item.setFeedbackCount(item.getFeedbackCount() + 1); |
| | | } |
| | | if (feedback.getFeedbackTime() != null) { |
| | | LocalDate date = feedback.getFeedbackTime().toLocalDate(); |
| | | MesProFeedbackSummaryRespVO.DateSummaryItem item = dateMap.computeIfAbsent(date, key -> { |
| | | MesProFeedbackSummaryRespVO.DateSummaryItem d = new MesProFeedbackSummaryRespVO.DateSummaryItem(); |
| | | d.setDate(key); |
| | | d.setFeedbackQuantity(BigDecimal.ZERO); |
| | | d.setQualifiedQuantity(BigDecimal.ZERO); |
| | | d.setFeedbackCount(0); |
| | | return d; |
| | | }); |
| | | item.setFeedbackQuantity(item.getFeedbackQuantity().add(nullToZero(feedback.getFeedbackQuantity()))); |
| | | item.setQualifiedQuantity(item.getQualifiedQuantity().add(nullToZero(feedback.getQualifiedQuantity()))); |
| | | item.setFeedbackCount(item.getFeedbackCount() + 1); |
| | | } |
| | | } |
| | | |
| | | // 7. 组装响应 |
| | | MesProFeedbackSummaryRespVO respVO = new MesProFeedbackSummaryRespVO(); |
| | | respVO.setTotalFeedbackQuantity(totalFeedbackQuantity); |
| | | respVO.setTotalQualifiedQuantity(totalQualifiedQuantity); |
| | | respVO.setTotalFeedbackCount(totalCount); |
| | | respVO.setByType(new ArrayList<>(typeMap.values())); |
| | | respVO.setByDate(new ArrayList<>(dateMap.values())); |
| | | return respVO; |
| | | } |
| | | |
| | | private static BigDecimal nullToZero(BigDecimal value) { |
| | | return value == null ? BigDecimal.ZERO : value; |
| | | } |
| | | |
| | | } |