| | |
| | | import cn.hutool.core.util.ObjUtil; |
| | | import cn.hutool.core.util.ObjectUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO; |
| | |
| | | import cn.iocoder.yudao.module.mes.enums.DictTypeConstants; |
| | | import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum; |
| | | import cn.iocoder.yudao.module.mes.service.qc.imports.MesQcImportHelper; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO; |
| | |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.qc.defectrecord.MesQcDefectRecordDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper; |
| | | import cn.iocoder.yudao.module.mes.enums.qc.MesQcDefectLevelEnum; |
| | |
| | | import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService; |
| | | import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService; |
| | | import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService; |
| | | import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateService; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.service.qc.ncr.MesQcNcrService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService; |
| | |
| | | /** |
| | | * IPQC 保存数据校验上下文 |
| | | * |
| | | * @param workstation 工位 |
| | | * @param workstation 工位(可选,未指定 workstationId 时为空) |
| | | * @param task 生产任务(可选,指定 taskId 时非空) |
| | | * @param workOrder 生产工单(可选,未指定 workOrderId 时为空) |
| | | */ |
| | | private record SaveDataContext(MesMdWorkstationDO workstation, MesProTaskDO task) {} |
| | | private record SaveDataContext(MesMdWorkstationDO workstation, MesProTaskDO task, MesProWorkOrderDO workOrder) {} |
| | | |
| | | @Resource |
| | | private MesQcIpqcMapper ipqcMapper; |
| | | |
| | | @Resource |
| | | private MesQcTemplateItemService templateItemService; |
| | | @Resource |
| | | private MesQcTemplateService templateService; |
| | | @Resource |
| | | private MesQcIpqcLineService ipqcLineService; |
| | | @Resource |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long createIpqc(MesQcIpqcSaveReqVO createReqVO) { |
| | | // 1.1 校验数据 |
| | | // 1.1 校验数据(生产订单、工位均非必填) |
| | | SaveDataContext ctx = validateIpqcSaveData(createReqVO); |
| | | // 1.2 校验工单存在 |
| | | MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderExists(createReqVO.getWorkOrderId()); |
| | | // 1.3 确定检验物料:有任务时取任务的物料,无任务时取工单的产品 |
| | | Long itemId = (ctx.task() != null) ? ctx.task().getItemId() : workOrder.getProductId(); |
| | | // 1.4 根据物料 + IPQC 类型查找可用质检方案(支持前端显式指定) |
| | | List<MesQcTemplateItemDO> templateItems = templateItemService.getTemplateItemList( |
| | | itemId, MesQcTypeEnum.IPQC.getType(), null, null); |
| | | if (CollUtil.isEmpty(templateItems)) { |
| | | throw exception(QC_NO_TEMPLATE); |
| | | } |
| | | MesQcTemplateItemDO templateItem = templateItems.get(0); |
| | | if (createReqVO.getTemplateId() != null) { |
| | | templateItem = templateItems.stream() |
| | | .filter(available -> Objects.equals(available.getTemplateId(), createReqVO.getTemplateId())) |
| | | .findFirst().orElse(null); |
| | | if (templateItem == null) { |
| | | throw exception(QC_NO_TEMPLATE); // 指定了方案但不在匹配列表中,拒绝 |
| | | } |
| | | } |
| | | Long templateId = templateItem.getTemplateId(); |
| | | // 1.5 获取来源单据编号 |
| | | // 1.2 确定检验物料:有任务时取任务的物料,有工单时取工单的产品,都没有时取请求指定的产品物料 |
| | | Long itemId = resolveItemId(createReqVO, ctx); |
| | | // 1.3 确定质检方案:显式指定时校验方案本身可用,未指定时按产品物料已绑定方案兜底 |
| | | Long templateId = resolveTemplateId(itemId, createReqVO.getTemplateId()); |
| | | // 1.4 获取来源单据编号 |
| | | String sourceDocCode = validateAndGetSourceDocCode( |
| | | createReqVO.getSourceDocType(), createReqVO.getSourceDocId(), createReqVO.getSourceLineId()); |
| | | |
| | | // 2. 插入主表 |
| | | MesQcIpqcDO ipqc = BeanUtils.toBean(createReqVO, MesQcIpqcDO.class) |
| | | .setItemId(itemId) |
| | | .setProcessId(getProcessId(itemId, ctx.workstation().getProcessId())) |
| | | .setProcessId(getProcessId(itemId, |
| | | ctx.workstation() != null ? ctx.workstation().getProcessId() : null)) |
| | | .setTemplateId(templateId) |
| | | .setSourceDocCode(sourceDocCode) |
| | | .setStatus(MesQcStatusEnum.DRAFT.getStatus()); |
| | |
| | | updateObj.setTemplateId(null); // 不允许修改模板 |
| | | updateObj.setItemId(null); // 不允许修改物料 |
| | | updateObj.setWorkOrderId(null); // 不允许修改工单 |
| | | updateObj.setProcessId(getProcessId(existIpqc.getItemId(), ctx.workstation().getProcessId())); |
| | | updateObj.setProcessId(getProcessId(existIpqc.getItemId(), |
| | | ctx.workstation() != null ? ctx.workstation().getProcessId() : null)); |
| | | ipqcMapper.updateById(updateObj); |
| | | } |
| | | |
| | | private SaveDataContext validateIpqcSaveData(MesQcIpqcSaveReqVO reqVO) { |
| | | // 校验编号唯一 |
| | | validateIpqcCodeUnique(reqVO.getId(), reqVO.getCode()); |
| | | // 校验工位、检测人员存在 |
| | | MesMdWorkstationDO workstation = workstationService.validateWorkstationExists(reqVO.getWorkstationId()); |
| | | // 校验工位存在(工位非必填,未选择时跳过) |
| | | MesMdWorkstationDO workstation = reqVO.getWorkstationId() != null |
| | | ? workstationService.validateWorkstationExists(reqVO.getWorkstationId()) : null; |
| | | adminUserApi.validateUser(reqVO.getInspectorUserId()); |
| | | // 校验工单存在 |
| | | MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId()); |
| | | if (reqVO.getProcessId() != null && ObjUtil.notEqual(workstation.getProcessId(), reqVO.getProcessId())) { |
| | | // 校验工单存在且已确认(生产订单非必填,未选择时跳过) |
| | | MesProWorkOrderDO workOrder = reqVO.getWorkOrderId() != null |
| | | ? workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId()) : null; |
| | | if (workstation != null && reqVO.getProcessId() != null |
| | | && ObjUtil.notEqual(workstation.getProcessId(), reqVO.getProcessId())) { |
| | | throw exception(PRO_WORKSTATION_PROCESS_MISMATCH); |
| | | } |
| | | // 校验任务关系(如果指定了任务) |
| | |
| | | if (reqVO.getTaskId() != null) { |
| | | task = taskService.validateTaskNotFinished(reqVO.getTaskId()); |
| | | validateTaskRelation(task, workstation, workOrder, reqVO); |
| | | } else { |
| | | } else if (workOrder != null && reqVO.getItemId() != null |
| | | && ObjUtil.notEqual(reqVO.getItemId(), workOrder.getProductId())) { |
| | | // 无任务时,itemId 仍需与工单产品一致 |
| | | if (reqVO.getItemId() != null && ObjUtil.notEqual(reqVO.getItemId(), workOrder.getProductId())) { |
| | | throw exception(PRO_WORK_ORDER_PRODUCT_MISMATCH); |
| | | } |
| | | throw exception(PRO_WORK_ORDER_PRODUCT_MISMATCH); |
| | | } |
| | | return new SaveDataContext(workstation, task); |
| | | return new SaveDataContext(workstation, task, workOrder); |
| | | } |
| | | |
| | | /** |
| | | * 确定检验物料:任务优先,其次工单产品,都没有时取请求指定的产品物料(都未指定则为空) |
| | | */ |
| | | private Long resolveItemId(MesQcIpqcSaveReqVO reqVO, SaveDataContext ctx) { |
| | | if (ctx.task() != null) { |
| | | return ctx.task().getItemId(); |
| | | } |
| | | if (ctx.workOrder() != null) { |
| | | return ctx.workOrder().getProductId(); |
| | | } |
| | | return reqVO.getItemId(); |
| | | } |
| | | |
| | | /** |
| | | * 确定质检方案 |
| | | * |
| | | * <p>产品物料已绑定 IPQC 方案时沿用原逻辑:未显式指定取绑定列表首个,显式指定则必须命中绑定列表; |
| | | * <p>产品物料未匹配到任何方案(未选生产订单/生产任务/产品物料)时,显式指定的方案改为校验方案本身: |
| | | * 存在、已启用、检测种类包含过程检验,不再要求与产品物料绑定。 |
| | | */ |
| | | private Long resolveTemplateId(Long itemId, Long explicitTemplateId) { |
| | | List<MesQcTemplateItemDO> templateItems = templateItemService.getTemplateItemList( |
| | | itemId, MesQcTypeEnum.IPQC.getType(), null, null); |
| | | if (CollUtil.isNotEmpty(templateItems)) { |
| | | if (explicitTemplateId == null) { |
| | | return templateItems.get(0).getTemplateId(); |
| | | } |
| | | return templateItems.stream() |
| | | .filter(available -> Objects.equals(available.getTemplateId(), explicitTemplateId)) |
| | | .findFirst() |
| | | .orElseThrow(() -> exception(QC_NO_TEMPLATE)) // 指定了方案但不在匹配列表中,拒绝 |
| | | .getTemplateId(); |
| | | } |
| | | // 产品物料未匹配到任何 IPQC 方案 |
| | | if (explicitTemplateId != null) { |
| | | return validateTemplateUsable(explicitTemplateId); |
| | | } |
| | | throw new ServiceException(QC_IPQC_NO_TEMPLATE.getCode(), buildNoTemplateMessage(itemId)); |
| | | } |
| | | |
| | | /** |
| | | * 校验质检方案本身可用:存在、已启用、检测种类包含过程检验,返回方案编号 |
| | | */ |
| | | private Long validateTemplateUsable(Long templateId) { |
| | | MesQcTemplateDO template = templateService.getTemplate(templateId); |
| | | if (template == null) { |
| | | throw new ServiceException(QC_TEMPLATE_NOT_EXISTS.getCode(), StrUtil.format( |
| | | "所选质检方案不存在(ID={}),可能已被删除,请刷新后重新选择质检方案", templateId)); |
| | | } |
| | | if (!CommonStatusEnum.isEnable(template.getStatus())) { |
| | | throw exception(QC_IPQC_TEMPLATE_IS_DISABLE, template.getName()); |
| | | } |
| | | if (!CollUtil.contains(template.getTypes(), MesQcTypeEnum.IPQC.getType())) { |
| | | throw exception(QC_IPQC_TEMPLATE_TYPE_NOT_MATCH, template.getName()); |
| | | } |
| | | return template.getId(); |
| | | } |
| | | |
| | | /** |
| | | * 构建「无可用质检方案」的提示:区分「有产品物料但未绑定方案」与「未选工单/任务/物料」 |
| | | */ |
| | | private String buildNoTemplateMessage(Long itemId) { |
| | | if (itemId == null) { |
| | | return "未选择生产订单/生产任务/产品物料,无法自动匹配质检方案:" |
| | | + "请直接选择「质检方案」后重试,或先选择生产订单/生产任务以自动带出该产品已绑定的方案。"; |
| | | } |
| | | return StrUtil.format("产品物料[{}]未绑定「过程检验」质检方案:请在【质量管理-质检方案】中为该物料" |
| | | + "配置检测种类包含过程检验的方案,或在本单手动选择质检方案。", describeItem(itemId)); |
| | | } |
| | | |
| | | /** |
| | | * 描述产品物料,用于异常提示:优先给出「编码 名称」,查不到时回退为物料编号 |
| | | */ |
| | | private String describeItem(Long itemId) { |
| | | MesMdItemDO item = itemService.getItem(itemId); |
| | | return item != null ? item.getCode() + " " + item.getName() : "物料编号 ID=" + itemId; |
| | | } |
| | | |
| | | private void validateTaskRelation(MesProTaskDO task, MesMdWorkstationDO workstation, |
| | | MesProWorkOrderDO workOrder, MesQcIpqcSaveReqVO reqVO) { |
| | | if (ObjUtil.notEqual(task.getWorkOrderId(), workOrder.getId())) { |
| | | // 生产订单、工位均非必填,未选择时跳过对应比对 |
| | | if (workOrder != null && ObjUtil.notEqual(task.getWorkOrderId(), workOrder.getId())) { |
| | | throw exception(PRO_TASK_WORK_ORDER_MISMATCH); |
| | | } |
| | | if (ObjUtil.notEqual(task.getWorkstationId(), workstation.getId())) { |
| | | if (workstation != null && ObjUtil.notEqual(task.getWorkstationId(), workstation.getId())) { |
| | | throw exception(PRO_TASK_WORKSTATION_MISMATCH); |
| | | } |
| | | Long expectedProcessId = reqVO.getProcessId() != null ? reqVO.getProcessId() : workstation.getProcessId(); |
| | | Long expectedProcessId = reqVO.getProcessId() != null ? reqVO.getProcessId() |
| | | : (workstation != null ? workstation.getProcessId() : null); |
| | | if (expectedProcessId != null && ObjUtil.notEqual(task.getProcessId(), expectedProcessId)) { |
| | | throw exception(PRO_TASK_ROUTE_PROCESS_MISMATCH); |
| | | } |
| | |
| | | + "。请修改模板中的「检验单编号」,或先在系统中删除该编号的检验单后重新导入"); |
| | | } |
| | | reqVO.setCode(code); |
| | | // 2. 名称、检验类型、工单、工位、质检方案 |
| | | // 2. 名称、检验类型、工单、工位、质检方案(生产工单号、工位名称留空即不关联,不再拦截) |
| | | if (StrUtil.isBlank(row.getName())) { |
| | | throw new IllegalArgumentException("检验单名称不能为空:请填写模板中该行的「检验单名称」列"); |
| | | } |