| | |
| | | package cn.iocoder.yudao.module.mes.service.qc.ipqc; |
| | | |
| | | 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.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.wm.MesWmQualityStatusEnum; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcImportExcelVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcPageReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.vo.MesQcImportRespVO; |
| | | 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.enums.qc.MesQcStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; |
| | | import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; |
| | | import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService; |
| | |
| | | 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; |
| | | import cn.iocoder.yudao.module.system.api.user.AdminUserApi; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.context.annotation.Lazy; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.util.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | |
| | | /** |
| | | * MES 过程检验单(IPQC) Service 实现类 |
| | | * |
| | | * @author 芋道源码 |
| | | * @author 超级管理员 |
| | | */ |
| | | @Service |
| | | @Validated |
| | | @Slf4j |
| | | public class MesQcIpqcServiceImpl implements MesQcIpqcService { |
| | | |
| | | /** |
| | | * 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 |
| | |
| | | private MesWmProductProduceLineService productProduceLineService; |
| | | |
| | | @Resource |
| | | @Lazy |
| | | private MesQcNcrService ncrService; |
| | | |
| | | @Resource |
| | | private AdminUserApi adminUserApi; |
| | | |
| | | @Resource |
| | | private MesQcImportHelper qcImportHelper; |
| | | |
| | | @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 根据物料 + 检验类型自动匹配模板 |
| | | MesQcTemplateItemDO templateItem = templateItemService.getRequiredTemplateByItemIdAndType( |
| | | itemId, MesQcTypeEnum.IPQC.getType()); |
| | | 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); |
| | | } |
| | |
| | | ipqcMapper.updateById(new MesQcIpqcDO() |
| | | .setId(id).setStatus(MesQcStatusEnum.FINISHED.getStatus())); |
| | | |
| | | // 3. 回写来源单据 |
| | | // 3. 不合格时自动创建 NCR |
| | | generateNcrIfNeeded(ipqc); |
| | | |
| | | // 4. 回写来源单据 |
| | | writeBackSourceDoc(ipqc); |
| | | } |
| | | |
| | | /** |
| | | * 不合格时自动创建 NCR(不合格品处理单) |
| | | * |
| | | * @param ipqc 过程检验单 |
| | | */ |
| | | private void generateNcrIfNeeded(MesQcIpqcDO ipqc) { |
| | | BigDecimal unqualifiedQty = ipqc.getUnqualifiedQuantity(); |
| | | if (unqualifiedQty == null || unqualifiedQty.compareTo(BigDecimal.ZERO) <= 0) { |
| | | return; |
| | | } |
| | | String ncrCode = "NCR" + java.time.LocalDateTime.now() |
| | | .format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); |
| | | MesQcNcrSaveReqVO saveReqVO = new MesQcNcrSaveReqVO(); |
| | | saveReqVO.setCode(ncrCode); |
| | | saveReqVO.setName("不合格品处理单"); |
| | | saveReqVO.setQcType(MesQcTypeEnum.IPQC.getType()); |
| | | saveReqVO.setQcId(ipqc.getId()); |
| | | saveReqVO.setItemId(ipqc.getItemId()); |
| | | saveReqVO.setUnqualifiedQuantity(unqualifiedQty); |
| | | saveReqVO.setDefectDescription("由过程检验(" + ipqc.getCode() + ")自动生成"); |
| | | ncrService.createNcr(saveReqVO); |
| | | log.info("[generateNcrIfNeeded] IPQC({}) 不合格,自动创建 NCR", ipqc.getId()); |
| | | } |
| | | |
| | | /** |
| | |
| | | * @param ipqc 过程检验单 |
| | | */ |
| | | private void writeBackSourceDoc(MesQcIpqcDO ipqc) { |
| | | if (ipqc.getSourceDocType() == null) { |
| | | return; |
| | | } |
| | | if (ipqc.getSourceDocId() == null) { |
| | | throw new IllegalArgumentException( |
| | | "IPQC 单[" + ipqc.getId() + "] sourceDocType 非空但 sourceDocId 为空"); |
| | | // 1. 确定 sourceDocType/sourceDocId |
| | | Long sourceDocId = ipqc.getSourceDocId(); |
| | | Long sourceLineId = ipqc.getSourceLineId(); |
| | | |
| | | // 如果 IPQC 单没有设置来源,尝试通过 taskId 查找关联的待检验报工 |
| | | if (sourceDocId == null && ipqc.getTaskId() != null) { |
| | | MesProFeedbackDO feedback = feedbackService.getFeedbackListByTaskId(ipqc.getTaskId()).stream() |
| | | .filter(f -> MesProFeedbackStatusEnum.UNCHECK.getStatus().equals(f.getStatus())) |
| | | .findFirst().orElse(null); |
| | | if (feedback != null) { |
| | | sourceDocId = feedback.getId(); |
| | | log.info("[writeBackSourceDoc] IPQC({}) 未设置来源单据,通过 taskId({}) 找到待检验报工({})", |
| | | ipqc.getId(), ipqc.getTaskId(), feedback.getId()); |
| | | } |
| | | } |
| | | |
| | | if (Objects.equals(ipqc.getSourceDocType(), MesBizTypeConstants.PRO_FEEDBACK)) { |
| | | feedbackService.updateProFeedbackWhenIpqcFinish(ipqc.getSourceDocId(), ipqc.getSourceLineId(), |
| | | ObjectUtil.defaultIfNull(ipqc.getQualifiedQuantity(), BigDecimal.ZERO), |
| | | ObjectUtil.defaultIfNull(ipqc.getUnqualifiedQuantity(), BigDecimal.ZERO), |
| | | ObjectUtil.defaultIfNull(ipqc.getLaborScrapQuantity(), BigDecimal.ZERO), |
| | | ObjectUtil.defaultIfNull(ipqc.getMaterialScrapQuantity(), BigDecimal.ZERO), |
| | | ObjectUtil.defaultIfNull(ipqc.getOtherScrapQuantity(), BigDecimal.ZERO)); |
| | | } else { |
| | | throw new IllegalArgumentException( |
| | | "IPQC 单[" + ipqc.getId() + "] sourceDocType=" + ipqc.getSourceDocType() + " 无法识别,无法回写来源单据"); |
| | | if (sourceDocId == null) { |
| | | log.warn("[writeBackSourceDoc] IPQC({}) 无法确定来源报工,跳过回写", ipqc.getId()); |
| | | return; |
| | | } |
| | | |
| | | // 2. 回写报工 |
| | | feedbackService.updateProFeedbackWhenIpqcFinish(sourceDocId, sourceLineId, |
| | | ObjectUtil.defaultIfNull(ipqc.getQualifiedQuantity(), BigDecimal.ZERO), |
| | | ObjectUtil.defaultIfNull(ipqc.getUnqualifiedQuantity(), BigDecimal.ZERO), |
| | | ObjectUtil.defaultIfNull(ipqc.getLaborScrapQuantity(), BigDecimal.ZERO), |
| | | ObjectUtil.defaultIfNull(ipqc.getMaterialScrapQuantity(), BigDecimal.ZERO), |
| | | ObjectUtil.defaultIfNull(ipqc.getOtherScrapQuantity(), BigDecimal.ZERO)); |
| | | } |
| | | |
| | | @Override |
| | |
| | | ipqcMapper.updateById(updateIpqc); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public MesQcImportRespVO importIpqcList(List<MesQcIpqcImportExcelVO> importList) { |
| | | MesQcImportRespVO respVO = MesQcImportRespVO.builder() |
| | | .createList(new ArrayList<>()).failureList(new LinkedHashMap<>()).build(); |
| | | if (CollUtil.isEmpty(importList)) { |
| | | return respVO; |
| | | } |
| | | qcImportHelper.startImport(); |
| | | Map<String, Integer> firstRowNoByKey = new HashMap<>(); |
| | | for (int i = 0; i < importList.size(); i++) { |
| | | MesQcIpqcImportExcelVO row = importList.get(i); |
| | | if (isImportRowEmpty(row)) { |
| | | continue; // 跳过空行 |
| | | } |
| | | String label = StrUtil.isNotBlank(row.getCode()) ? "检验单编号 " + row.getCode().trim() |
| | | : "第 " + (i + 2) + " 行"; |
| | | String importKey = buildImportKey(row); |
| | | try { |
| | | Integer firstRowNo = firstRowNoByKey.get(importKey); |
| | | if (firstRowNo != null) { |
| | | throw MesQcImportHelper.duplicateRowException(firstRowNo, buildImportKeyDesc(row)); |
| | | } |
| | | respVO.getCreateList().add(createIpqcByImport(row)); |
| | | firstRowNoByKey.putIfAbsent(importKey, i + 2); |
| | | } catch (Exception ex) { |
| | | respVO.getFailureList().put(label, MesQcImportHelper.resolveImportErrorMessage(ex)); |
| | | } |
| | | } |
| | | return respVO; |
| | | } |
| | | |
| | | private boolean isImportRowEmpty(MesQcIpqcImportExcelVO row) { |
| | | return row == null || (StrUtil.isBlank(row.getCode()) && StrUtil.isBlank(row.getName()) |
| | | && StrUtil.isBlank(row.getType()) && StrUtil.isBlank(row.getWorkOrderCode()) |
| | | && StrUtil.isBlank(row.getWorkstationName()) && StrUtil.isBlank(row.getTemplateName()) |
| | | && row.getCheckQuantity() == null && row.getQualifiedQuantity() == null |
| | | && row.getUnqualifiedQuantity() == null && row.getLaborScrapQuantity() == null |
| | | && row.getMaterialScrapQuantity() == null && row.getOtherScrapQuantity() == null |
| | | && StrUtil.isBlank(row.getCheckResult()) && StrUtil.isBlank(row.getInspectorName()) |
| | | && StrUtil.isBlank(row.getInspectDate()) && StrUtil.isBlank(row.getRemark())); |
| | | } |
| | | |
| | | /** |
| | | * 依据导入行创建一张过程检验单:物料由工单推导,状态为「待检验」,检验行由质检方案自动生成 |
| | | * |
| | | * @return 检验单编号 |
| | | */ |
| | | private String createIpqcByImport(MesQcIpqcImportExcelVO row) { |
| | | MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO(); |
| | | // 1. 检验单编号:填写则使用,留空自动生成 |
| | | String code = StrUtil.isNotBlank(row.getCode()) ? row.getCode().trim() |
| | | : qcImportHelper.generateCode(MesMdAutoCodeRuleCodeEnum.QC_IPQC_CODE.getCode()); |
| | | if (ipqcMapper.selectByCode(code) != null) { |
| | | throw new IllegalArgumentException("检验单编号已存在:" + code |
| | | + "。请修改模板中的「检验单编号」,或先在系统中删除该编号的检验单后重新导入"); |
| | | } |
| | | reqVO.setCode(code); |
| | | // 2. 名称、检验类型、工单、工位、质检方案(生产工单号、工位名称留空即不关联,不再拦截) |
| | | if (StrUtil.isBlank(row.getName())) { |
| | | throw new IllegalArgumentException("检验单名称不能为空:请填写模板中该行的「检验单名称」列"); |
| | | } |
| | | reqVO.setName(row.getName().trim()); |
| | | reqVO.setType(qcImportHelper.resolveDictValue(row.getType(), |
| | | DictTypeConstants.MES_IPQC_TYPE, "检验类型", true)); |
| | | reqVO.setWorkOrderId(qcImportHelper.resolveWorkOrderId(row.getWorkOrderCode())); |
| | | reqVO.setWorkstationId(qcImportHelper.resolveWorkstationId(row.getWorkstationName())); |
| | | reqVO.setTemplateId(qcImportHelper.resolveTemplateId(row.getTemplateName())); |
| | | // 3. 数量:合格品 + 不合格品 必须等于检测数量(检测数量留空则自动合计);工废/料废/其他废留空按 0 |
| | | BigDecimal qualified = qcImportHelper.resolveQuantity(row.getQualifiedQuantity(), "合格品数量", true); |
| | | BigDecimal unqualified = qcImportHelper.resolveQuantity(row.getUnqualifiedQuantity(), "不合格品数量", true); |
| | | reqVO.setQualifiedQuantity(qualified); |
| | | reqVO.setUnqualifiedQuantity(unqualified); |
| | | reqVO.setCheckQuantity(qcImportHelper.resolveCheckQuantity( |
| | | row.getCheckQuantity(), qualified, unqualified, "检测数量")); |
| | | BigDecimal laborScrap = qcImportHelper.resolveQuantity(row.getLaborScrapQuantity(), "工废数量", false); |
| | | BigDecimal materialScrap = qcImportHelper.resolveQuantity(row.getMaterialScrapQuantity(), "料废数量", false); |
| | | BigDecimal otherScrap = qcImportHelper.resolveQuantity(row.getOtherScrapQuantity(), "其他废品数量", false); |
| | | laborScrap = laborScrap != null ? laborScrap : BigDecimal.ZERO; |
| | | materialScrap = materialScrap != null ? materialScrap : BigDecimal.ZERO; |
| | | otherScrap = otherScrap != null ? otherScrap : BigDecimal.ZERO; |
| | | validateScrapQuantity(unqualified, laborScrap, materialScrap, otherScrap); |
| | | reqVO.setLaborScrapQuantity(laborScrap); |
| | | reqVO.setMaterialScrapQuantity(materialScrap); |
| | | reqVO.setOtherScrapQuantity(otherScrap); |
| | | // 4. 检验信息 |
| | | reqVO.setCheckResult(qcImportHelper.resolveDictValue(row.getCheckResult(), |
| | | DictTypeConstants.MES_QC_CHECK_RESULT, "检测结果", false)); |
| | | reqVO.setInspectorUserId(qcImportHelper.resolveInspectorUserId(row.getInspectorName())); |
| | | reqVO.setInspectDate(qcImportHelper.parseDateTime(row.getInspectDate(), "检测日期")); |
| | | reqVO.setRemark(StrUtil.trimToNull(row.getRemark())); |
| | | // 5. 去重:同一 生产工单 + 检验类型 + 工位 + 检测日期 的过程检验单不可重复导入 |
| | | MesQcIpqcDO duplicate = CollUtil.getFirst(ipqcMapper.selectListByImportKey(reqVO.getWorkOrderId(), |
| | | reqVO.getType(), reqVO.getWorkstationId(), reqVO.getInspectDate())); |
| | | if (duplicate != null) { |
| | | throw MesQcImportHelper.duplicateExistingException(buildImportKeyDesc(row), "过程检验单", |
| | | duplicate.getCode(), "质量管理-过程检验"); |
| | | } |
| | | // 6. 复用创建逻辑:校验工单/工位/人员、按物料匹配质检方案、生成检验行 |
| | | try { |
| | | createIpqc(reqVO); |
| | | } catch (ServiceException ex) { |
| | | MesQcImportHelper.rethrowTemplateError(ex, row.getWorkOrderCode(), "过程检验(IPQC)"); |
| | | } |
| | | return code; |
| | | } |
| | | |
| | | /** |
| | | * 校验 工废 + 料废 + 其他废品 不超过不合格品数量 |
| | | */ |
| | | private void validateScrapQuantity(BigDecimal unqualified, BigDecimal laborScrap, |
| | | BigDecimal materialScrap, BigDecimal otherScrap) { |
| | | BigDecimal scrapSum = laborScrap.add(materialScrap).add(otherScrap); |
| | | if (scrapSum.compareTo(unqualified) <= 0) { |
| | | return; |
| | | } |
| | | throw new IllegalArgumentException("工废、料废、其他废品数量之和不能超过不合格品数量:不合格品数量为 " |
| | | + unqualified.stripTrailingZeros().toPlainString() + ",而 工废 " + laborScrap.stripTrailingZeros().toPlainString() |
| | | + " + 料废 " + materialScrap.stripTrailingZeros().toPlainString() + " + 其他废品 " |
| | | + otherScrap.stripTrailingZeros().toPlainString() + " = " + scrapSum.stripTrailingZeros().toPlainString() |
| | | + "。请修改模板中的废品数量或不合格品数量后重新导入"); |
| | | } |
| | | |
| | | /** |
| | | * 导入去重键:同一 生产工单 + 检验类型 + 工位 + 检测日期 视为同一批过程检验数据 |
| | | */ |
| | | private String buildImportKey(MesQcIpqcImportExcelVO row) { |
| | | return String.join("|", StrUtil.trimToEmpty(row.getWorkOrderCode()), StrUtil.trimToEmpty(row.getType()), |
| | | StrUtil.trimToEmpty(row.getWorkstationName()), StrUtil.trimToEmpty(row.getInspectDate())); |
| | | } |
| | | |
| | | private String buildImportKeyDesc(MesQcIpqcImportExcelVO row) { |
| | | return "生产工单[" + MesQcImportHelper.describeImportValue(row.getWorkOrderCode()) |
| | | + "]、检验类型[" + MesQcImportHelper.describeImportValue(row.getType()) |
| | | + "]、工位[" + MesQcImportHelper.describeImportValue(row.getWorkstationName()) |
| | | + "]、检测日期[" + MesQcImportHelper.describeImportValue(row.getInspectDate()) + "]"; |
| | | } |
| | | |
| | | } |