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.workorder.MesProWorkOrderDO; 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.pro.feedback.MesProFeedbackService; import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService; import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService; import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService; import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService; 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 org.springframework.validation.annotation.Validated; 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; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; /** * MES 过程检验单(IPQC) Service 实现类 * * @author 超级管理员 */ @Service @Validated @Slf4j public class MesQcIpqcServiceImpl implements MesQcIpqcService { /** * IPQC 保存数据校验上下文 * * @param workstation 工位(可选,未指定 workstationId 时为空) * @param task 生产任务(可选,指定 taskId 时非空) * @param workOrder 生产工单(可选,未指定 workOrderId 时为空) */ 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 @Lazy private MesProWorkOrderService workOrderService; @Resource @Lazy private MesMdWorkstationService workstationService; @Resource @Lazy private MesMdItemService itemService; @Resource @Lazy private MesQcDefectRecordService defectRecordService; @Resource @Lazy private MesProRouteProductService routeProductService; @Resource @Lazy private MesProRouteProcessService routeProcessService; @Resource @Lazy private MesProTaskService taskService; @Resource @Lazy private MesProFeedbackService feedbackService; @Resource @Lazy private MesQcIndicatorResultService indicatorResultService; @Resource @Lazy 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 校验数据(生产订单、工位均非必填) SaveDataContext ctx = validateIpqcSaveData(createReqVO); // 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() != null ? ctx.workstation().getProcessId() : null)) .setTemplateId(templateId) .setSourceDocCode(sourceDocCode) .setStatus(MesQcStatusEnum.DRAFT.getStatus()); ipqcMapper.insert(ipqc); // 3. 从模板指标自动生成检验行 ipqcLineService.createLinesFromTemplate(ipqc.getId(), templateId); return ipqc.getId(); } @Override public void updateIpqc(MesQcIpqcSaveReqVO updateReqVO) { // 1.1 校验存在 + 草稿状态 validateIpqcStatusPrepare(updateReqVO.getId()); // 1.2 校验数据 SaveDataContext ctx = validateIpqcSaveData(updateReqVO); // 2. 更新主表 MesQcIpqcDO existIpqc = validateIpqcExists(updateReqVO.getId()); MesQcIpqcDO updateObj = BeanUtils.toBean(updateReqVO, MesQcIpqcDO.class); updateObj.setSourceDocType(null).setSourceDocId(null).setSourceLineId(null); // 不允许修改来源单据 updateObj.setTemplateId(null); // 不允许修改模板 updateObj.setItemId(null); // 不允许修改物料 updateObj.setWorkOrderId(null); // 不允许修改工单 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 = reqVO.getWorkstationId() != null ? workstationService.validateWorkstationExists(reqVO.getWorkstationId()) : null; adminUserApi.validateUser(reqVO.getInspectorUserId()); // 校验工单存在且已确认(生产订单非必填,未选择时跳过) 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); } // 校验任务关系(如果指定了任务) MesProTaskDO task = null; if (reqVO.getTaskId() != null) { task = taskService.validateTaskNotFinished(reqVO.getTaskId()); validateTaskRelation(task, workstation, workOrder, reqVO); } else if (workOrder != null && reqVO.getItemId() != null && ObjUtil.notEqual(reqVO.getItemId(), workOrder.getProductId())) { // 无任务时,itemId 仍需与工单产品一致 throw exception(PRO_WORK_ORDER_PRODUCT_MISMATCH); } 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(); } /** * 确定质检方案 * *
产品物料已绑定 IPQC 方案时沿用原逻辑:未显式指定取绑定列表首个,显式指定则必须命中绑定列表; *
产品物料未匹配到任何方案(未选生产订单/生产任务/产品物料)时,显式指定的方案改为校验方案本身:
* 存在、已启用、检测种类包含过程检验,不再要求与产品物料绑定。
*/
private Long resolveTemplateId(Long itemId, Long explicitTemplateId) {
List 当来源为报工时,拆分待检产出行、完成产出入库、回写报工状态和任务/工单进度
*
* @param ipqc 过程检验单
*/
private void writeBackSourceDoc(MesQcIpqcDO ipqc) {
// 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 (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
@Transactional(rollbackFor = Exception.class)
public void deleteIpqc(Long id) {
// 1. 校验存在 + 草稿状态
validateIpqcStatusPrepare(id);
// 2.1 删除主表
ipqcMapper.deleteById(id);
// 2.2 级联删除行
ipqcLineService.deleteListByIpqcId(id);
// 2.3 级联删除缺陷记录
defectRecordService.deleteListByQcTypeAndQcId(MesQcTypeEnum.IPQC.getType(), id);
}
@Override
public MesQcIpqcDO validateIpqcExists(Long id) {
MesQcIpqcDO ipqc = ipqcMapper.selectById(id);
if (ipqc == null) {
throw exception(QC_IPQC_NOT_EXISTS);
}
return ipqc;
}
/**
* 校验过程检验单存在且为草稿状态
*
* @param id 过程检验单 ID
* @return 过程检验单
*/
private MesQcIpqcDO validateIpqcStatusPrepare(Long id) {
MesQcIpqcDO ipqc = validateIpqcExists(id);
if (ObjUtil.notEqual(ipqc.getStatus(), MesQcStatusEnum.DRAFT.getStatus())) {
throw exception(QC_IPQC_NOT_PREPARE);
}
return ipqc;
}
private void validateIpqcCodeUnique(Long id, String code) {
MesQcIpqcDO ipqc = ipqcMapper.selectByCode(code);
if (ipqc == null) {
return;
}
if (ObjUtil.notEqual(ipqc.getId(), id)) {
throw exception(QC_IPQC_CODE_DUPLICATE);
}
}
private String validateAndGetSourceDocCode(Integer sourceDocType, Long sourceDocId, Long sourceLineId) {
if (sourceDocType == null || sourceDocId == null) {
return null;
}
if (Objects.equals(sourceDocType, MesBizTypeConstants.PRO_FEEDBACK)) {
MesProFeedbackDO feedback = feedbackService.validateFeedbackExists(sourceDocId);
// 校验 sourceLineId 必填
if (sourceLineId == null) {
throw exception(QC_IPQC_SOURCE_LINE_REQUIRED);
}
// 校验 sourceLineId 存在,且属于该报工的产出行
MesWmProductProduceLineDO targetLine = productProduceLineService
.validateProductProduceLineExists(sourceLineId);
if (ObjUtil.notEqual(targetLine.getFeedbackId(), sourceDocId)) {
throw exception(QC_IPQC_SOURCE_LINE_NOT_BELONG);
}
// 校验该产出行为待检验状态
if (ObjUtil.notEqual(targetLine.getQualityStatus(), MesWmQualityStatusEnum.PENDING.getStatus())) {
throw exception(QC_IPQC_SOURCE_LINE_NOT_PENDING);
}
return feedback.getCode();
}
// 未知来源类型应报错,而不是静默忽略
throw exception(QC_IPQC_SOURCE_DOC_TYPE_UNKNOWN);
}
/**
* 推导工序 ID
*
* 通过产品 → 工艺路线产品 → 工艺路线 → 校验工位工序是否在该路线中
* 如果工位的工序在路线中,则返回该 processId;否则返回 null
*
* @param productId 产品 ID(工单产品)
* @param workstationProcessId 工位关联的工序 ID
* @return 推导出的工序 ID,无法推导时返回 null
*/
private Long getProcessId(Long productId, Long workstationProcessId) {
if (productId == null || workstationProcessId == null) {
return null;
}
// 1. 产品 → 工艺路线
MesProRouteProductDO routeProduct = routeProductService.getRouteProductByItemId(productId);
if (routeProduct == null) {
return null;
}
// 2. 校验工位的工序是否在该路线中
if (routeProcessService.getRouteProcessByRouteIdAndProcessId(
routeProduct.getRouteId(), workstationProcessId) == null) {
return null;
}
return workstationProcessId;
}
@Override
public MesQcIpqcDO getIpqc(Long id) {
return ipqcMapper.selectById(id);
}
@Override
public PageResult