package cn.iocoder.yudao.module.mes.service.qc.ncr;
|
|
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.bpm.api.task.BpmProcessInstanceApi;
|
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
|
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
|
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderSaveReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrDisposeReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrDisposeRespVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrPageReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrSaveReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchGenerateReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.wm.miscreceipt.vo.MesWmMiscReceiptSaveReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.wm.miscreceipt.vo.line.MesWmMiscReceiptLineSaveReqVO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ncr.MesQcNcrDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
|
import cn.iocoder.yudao.module.mes.dal.mysql.qc.ncr.MesQcNcrMapper;
|
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
|
import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderSourceTypeEnum;
|
import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderTypeEnum;
|
import cn.iocoder.yudao.module.mes.enums.qc.MesNcrDispositionEnum;
|
import cn.iocoder.yudao.module.mes.enums.qc.MesNcrStatusEnum;
|
import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
|
import cn.iocoder.yudao.module.mes.enums.wm.MesWmMiscReceiptTypeEnum;
|
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
|
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
|
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
|
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
|
import cn.iocoder.yudao.module.mes.service.qc.iqc.MesQcIqcService;
|
import cn.iocoder.yudao.module.mes.service.qc.ipqc.MesQcIpqcService;
|
import cn.iocoder.yudao.module.mes.service.qc.oqc.MesQcOqcService;
|
import cn.iocoder.yudao.module.mes.service.qc.rqc.MesQcRqcService;
|
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
|
import cn.iocoder.yudao.module.mes.service.wm.miscreceipt.MesWmMiscReceiptLineService;
|
import cn.iocoder.yudao.module.mes.service.wm.miscreceipt.MesWmMiscReceiptService;
|
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
|
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
|
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
|
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
|
import jakarta.annotation.Resource;
|
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.LocalDateTime;
|
import java.util.Collection;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.Set;
|
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
|
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
|
|
/**
|
* MES 不合格品处理单(NCR) Service 实现类
|
*
|
* @author 超级管理员
|
*/
|
@Service
|
@Validated
|
public class MesQcNcrServiceImpl implements MesQcNcrService {
|
|
/**
|
* NCR 协同审批流程分类/流程定义 key
|
*/
|
private static final String NCR_APPROVAL_PROCESS_KEY = "mes_ncr_approve";
|
|
/**
|
* 最终处置允许值:让步放行、返修、返工、报废
|
* <p>
|
* 退货(1)、降级使用(5) 仍保留在 {@link MesNcrDispositionEnum} 与字典中,用于历史数据显示,
|
* 但不再作为质量部最终处置的可选项。
|
*/
|
private static final Set<Integer> DISPOSITION_ALLOWED_TYPES = Set.of(
|
MesNcrDispositionEnum.SCRAP.getType(),
|
MesNcrDispositionEnum.REWORK.getType(),
|
MesNcrDispositionEnum.CONCESSION.getType(),
|
MesNcrDispositionEnum.REPAIR.getType());
|
|
@Resource
|
private MesQcNcrMapper ncrMapper;
|
|
@Resource
|
private MdmItemApi mdmItemApi;
|
|
@Resource
|
private BpmProcessInstanceApi processInstanceApi;
|
|
@Resource
|
@Lazy
|
private MesQcIqcService iqcService;
|
@Resource
|
@Lazy
|
private MesQcIpqcService ipqcService;
|
@Resource
|
@Lazy
|
private MesQcOqcService oqcService;
|
@Resource
|
@Lazy
|
private MesQcRqcService rqcService;
|
|
@Resource
|
@Lazy
|
private MesWmMiscReceiptService miscReceiptService;
|
@Resource
|
@Lazy
|
private MesWmMiscReceiptLineService miscReceiptLineService;
|
@Resource
|
@Lazy
|
private MesWmWarehouseService warehouseService;
|
@Resource
|
@Lazy
|
private MesWmWarehouseLocationService warehouseLocationService;
|
@Resource
|
@Lazy
|
private MesWmWarehouseAreaService warehouseAreaService;
|
@Resource
|
@Lazy
|
private MesWmBatchService batchService;
|
@Resource
|
@Lazy
|
private MesProWorkOrderService workOrderService;
|
@Resource
|
@Lazy
|
private MesMdmItemService mdmItemService;
|
@Resource
|
@Lazy
|
private MesMdItemService mesItemService;
|
@Resource
|
@Lazy
|
private MesMdAutoCodeRecordService autoCodeRecordService;
|
|
/**
|
* 杂项入库单来源单据类型:不合格品处理单
|
*/
|
private static final String MISC_RECEIPT_SOURCE_TYPE_NCR = "QC_NCR";
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Long createNcr(MesQcNcrSaveReqVO createReqVO) {
|
// 1. 校验数据
|
validateNcrCodeUnique(null, createReqVO.getCode());
|
validateItemExists(createReqVO.getItemId());
|
|
// 2. 插入主表
|
MesQcNcrDO ncr = BeanUtils.toBean(createReqVO, MesQcNcrDO.class)
|
.setStatus(MesNcrStatusEnum.PENDING_REVIEW.getStatus());
|
// 如果传入了 qcId 和 qcType,则查询来源检验单编号并冗余存储
|
if (createReqVO.getQcType() != null && createReqVO.getQcId() != null) {
|
ncr.setQcCode(lookupQcCode(createReqVO.getQcType(), createReqVO.getQcId()));
|
}
|
ncrMapper.insert(ncr);
|
return ncr.getId();
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void updateNcr(MesQcNcrSaveReqVO updateReqVO) {
|
// 1. 校验存在 + 只允许待评审状态编辑
|
MesQcNcrDO ncr = validateNcrStatusPendingReview(updateReqVO.getId());
|
validateNcrCodeUnique(updateReqVO.getId(), updateReqVO.getCode());
|
validateItemExists(updateReqVO.getItemId());
|
|
// 2. 更新主表(仅允许更新部分字段)
|
MesQcNcrDO updateObj = BeanUtils.toBean(updateReqVO, MesQcNcrDO.class);
|
updateObj.setQcType(null); // 不允许修改检验类型
|
updateObj.setQcId(null); // 不允许修改检验单
|
updateObj.setStatus(null); // 不允许修改状态
|
ncrMapper.updateById(updateObj);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void deleteNcr(Long id) {
|
// 校验存在 + 只允许删除待评审状态
|
validateNcrStatusPendingReview(id);
|
ncrMapper.deleteById(id);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void reviewNcr(Long id, Integer disposition, Long responsibleDeptId,
|
Integer defectLevel, String reviewOpinion) {
|
// 1. 校验存在 + 待评审状态
|
validateNcrStatusPendingReview(id);
|
validateDisposition(disposition);
|
|
// 2. 更新评审信息
|
MesQcNcrDO updateObj = new MesQcNcrDO()
|
.setId(id)
|
.setDisposition(disposition)
|
.setResponsibleDeptId(responsibleDeptId)
|
.setReviewOpinion(reviewOpinion)
|
.setReviewerUserId(getLoginUserId())
|
.setReviewDate(LocalDateTime.now())
|
.setStatus(MesNcrStatusEnum.REVIEWED.getStatus());
|
if (defectLevel != null) {
|
updateObj.setDefectLevel(defectLevel);
|
}
|
ncrMapper.updateById(updateObj);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public MesQcNcrDisposeRespVO disposeNcr(MesQcNcrDisposeReqVO disposeReqVO) {
|
// 1. 校验存在 + 已评审(审批通过)状态
|
MesQcNcrDO ncr = validateNcrExists(disposeReqVO.getId());
|
if (!MesNcrStatusEnum.REVIEWED.getStatus().equals(ncr.getStatus())) {
|
throw exception(QC_NCR_DISPOSE_STATUS_INVALID, ncr.getCode(), resolveStatusName(ncr.getStatus()));
|
}
|
// 2. 处置方式必填,且必须是合法枚举值
|
validateDisposition(disposeReqVO.getDisposition());
|
// 3. 不合格数量必须大于 0,否则无法生成入库单 / 生产订单
|
if (ncr.getUnqualifiedQuantity() == null
|
|| ncr.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) <= 0) {
|
throw exception(QC_NCR_DISPOSE_QUANTITY_INVALID, ncr.getUnqualifiedQuantity());
|
}
|
|
// 4. 按处置方式联动生成业务单据
|
MesQcNcrDisposeRespVO respVO;
|
if (Objects.equals(MesNcrDispositionEnum.CONCESSION.getType(), disposeReqVO.getDisposition())
|
|| Objects.equals(MesNcrDispositionEnum.SCRAP.getType(), disposeReqVO.getDisposition())) {
|
// 让步放行 / 报废:生成杂项入库单并自动完成入库
|
respVO = createMiscReceiptForDispose(ncr, disposeReqVO);
|
} else {
|
// 返工 / 返修:生成自行生产类型的生产订单
|
respVO = createReworkWorkOrderForDispose(ncr, disposeReqVO);
|
}
|
|
// 5. 更新处置信息(处置方式以本次处置选择为准并落库)
|
ncrMapper.updateById(new MesQcNcrDO()
|
.setId(ncr.getId())
|
.setDisposition(disposeReqVO.getDisposition())
|
.setHandlerUserId(disposeReqVO.getHandlerUserId())
|
.setHandleDate(LocalDateTime.now())
|
.setHandleResult(disposeReqVO.getHandleResult())
|
.setStatus(MesNcrStatusEnum.DISPOSED.getStatus()));
|
return respVO;
|
}
|
|
/**
|
* 让步放行 / 报废:生成杂项入库单,自动提交并完成入库
|
*/
|
private MesQcNcrDisposeRespVO createMiscReceiptForDispose(MesQcNcrDO ncr, MesQcNcrDisposeReqVO reqVO) {
|
boolean scrap = Objects.equals(MesNcrDispositionEnum.SCRAP.getType(), reqVO.getDisposition());
|
String dispositionName = scrap ? MesNcrDispositionEnum.SCRAP.getName() : MesNcrDispositionEnum.CONCESSION.getName();
|
MesWmMiscReceiptTypeEnum receiptType = scrap
|
? MesWmMiscReceiptTypeEnum.SCRAP : MesWmMiscReceiptTypeEnum.CONCESSION;
|
|
// 4.1 校验入库仓库 / 库区 / 库位
|
MesMdmItemDO item = mdmItemService.validateItemExists(ncr.getItemId());
|
if (reqVO.getWarehouseId() == null || reqVO.getLocationId() == null || reqVO.getAreaId() == null) {
|
throw new ServiceException(QC_NCR_DISPOSE_STORAGE_REQUIRED.getCode(),
|
String.format("处置方式[%s]需要入库到指定库位:当前仓库[%s]、库区[%s]、库位[%s]未选全,"
|
+ "请回到处置弹窗补选缺失项后重新提交",
|
dispositionName, resolveWarehouseName(reqVO.getWarehouseId()),
|
resolveLocationName(reqVO.getLocationId()), resolveAreaName(reqVO.getAreaId())));
|
}
|
try {
|
warehouseAreaService.validateWarehouseAreaExists(
|
reqVO.getWarehouseId(), reqVO.getLocationId(), reqVO.getAreaId());
|
} catch (ServiceException ex) {
|
throw new ServiceException(QC_NCR_DISPOSE_STORAGE_DISABLED.getCode(),
|
String.format("处置方式[%s]所选入库库位校验失败:仓库[%s]、库区[%s]、库位[%s]。原因:%s;"
|
+ "请回到处置弹窗重新选择仓库/库区/库位",
|
dispositionName, resolveWarehouseName(reqVO.getWarehouseId()),
|
resolveLocationName(reqVO.getLocationId()), resolveAreaName(reqVO.getAreaId()),
|
ex.getMessage()));
|
}
|
|
// 4.2 批次管理的物料:批次号必填,不存在时自动创建批次
|
String batchCode = StrUtil.trimToNull(reqVO.getBatchCode());
|
if (Boolean.TRUE.equals(item.getIsBatchManaged())) {
|
if (batchCode == null) {
|
throw new ServiceException(QC_NCR_DISPOSE_BATCH_REQUIRED.getCode(),
|
String.format("物料[%s]启用了批次管理,处置方式[%s]入库时必须填写批次号;"
|
+ "请在处置弹窗的[批次号]中填写后重试", buildItemLabel(item), dispositionName));
|
}
|
MesWmBatchGenerateReqVO batchReqVO = new MesWmBatchGenerateReqVO();
|
batchReqVO.setItemId(ncr.getItemId());
|
batchReqVO.setBatchCode(batchCode);
|
batchReqVO.setReceiptDate(LocalDateTime.now());
|
try {
|
batchService.getOrGenerateBatchCode(batchReqVO);
|
} catch (ServiceException ex) {
|
throw new ServiceException(ex.getCode(),
|
String.format("为物料[%s]准备批次[%s]失败:%s;请补充批次信息后重新处置",
|
buildItemLabel(item), batchCode, ex.getMessage()));
|
}
|
}
|
|
// 4.3 创建杂项入库单
|
String receiptCode = autoCodeRecordService.generateAutoCode(
|
MesMdAutoCodeRuleCodeEnum.WM_MISC_RECEIPT_CODE.getCode());
|
String receiptName = String.format("%s入库单-%s", dispositionName, ncr.getCode());
|
MesWmMiscReceiptSaveReqVO receiptReqVO = new MesWmMiscReceiptSaveReqVO();
|
receiptReqVO.setCode(receiptCode);
|
receiptReqVO.setName(receiptName);
|
receiptReqVO.setType(receiptType.getType());
|
receiptReqVO.setSourceDocType(MISC_RECEIPT_SOURCE_TYPE_NCR);
|
receiptReqVO.setSourceDocId(ncr.getId());
|
receiptReqVO.setSourceDocCode(ncr.getCode());
|
receiptReqVO.setReceiptDate(LocalDateTime.now());
|
receiptReqVO.setRemark(String.format("由不合格品处理单[%s]处置为[%s]自动生成并完成入库",
|
ncr.getCode(), dispositionName));
|
Long receiptId = miscReceiptService.createMiscReceipt(receiptReqVO);
|
|
// 4.4 创建入库行(同物料 / 不合格数量 / 目标库位 / 批次号)
|
MesWmMiscReceiptLineSaveReqVO lineReqVO = new MesWmMiscReceiptLineSaveReqVO();
|
lineReqVO.setReceiptId(receiptId);
|
lineReqVO.setItemId(ncr.getItemId());
|
lineReqVO.setQuantity(ncr.getUnqualifiedQuantity());
|
lineReqVO.setBatchCode(batchCode);
|
lineReqVO.setWarehouseId(reqVO.getWarehouseId());
|
lineReqVO.setLocationId(reqVO.getLocationId());
|
lineReqVO.setAreaId(reqVO.getAreaId());
|
lineReqVO.setRemark(String.format("来源不合格品处理单[%s]", ncr.getCode()));
|
miscReceiptLineService.createMiscReceiptLine(lineReqVO);
|
|
// 4.5 自动提交并完成入库(增加库存 + 记录流水)
|
miscReceiptService.submitMiscReceipt(receiptId);
|
miscReceiptService.finishMiscReceipt(receiptId);
|
|
MesQcNcrDisposeRespVO respVO = new MesQcNcrDisposeRespVO();
|
respVO.setDisposition(reqVO.getDisposition());
|
respVO.setDocType("MISC_RECEIPT");
|
respVO.setDocId(receiptId);
|
respVO.setDocCode(receiptCode);
|
respVO.setDocName(receiptName);
|
respVO.setMessage(String.format("已生成%s并完成入库,入库单号[%s]", receiptName, receiptCode));
|
return respVO;
|
}
|
|
/**
|
* 返工 / 返修:生成自行生产类型的生产订单(草稿态,BOM/条码自动生成,工艺路线人工补充)
|
*/
|
private MesQcNcrDisposeRespVO createReworkWorkOrderForDispose(MesQcNcrDO ncr, MesQcNcrDisposeReqVO reqVO) {
|
boolean rework = Objects.equals(MesNcrDispositionEnum.REWORK.getType(), reqVO.getDisposition());
|
String dispositionName = rework ? MesNcrDispositionEnum.REWORK.getName() : MesNcrDispositionEnum.REPAIR.getName();
|
|
// 生产订单的产品是 MES 产品主数据(mes_md_item),NCR 存的是 MDM 物料(mdm_item),需先映射
|
MesMdItemDO product = mesItemService.getItemByMdmItemId(ncr.getItemId());
|
if (product == null) {
|
// 兼容历史数据:MES 产品与 MDM 物料主键一致时直接按主键取
|
product = mesItemService.getItem(ncr.getItemId());
|
}
|
if (product == null) {
|
MesMdmItemDO mdmItem = mdmItemService.getItem(ncr.getItemId());
|
throw new ServiceException(QC_NCR_DISPOSE_PRODUCT_NOT_EXISTS.getCode(),
|
String.format("物料[%s]在 MES 产品主数据中不存在(MDM 物料 ID=%s),无法生成返工/返修生产订单;"
|
+ "请先在[物料产品]中维护该产品后再处置",
|
mdmItem != null ? buildItemLabel(mdmItem) : "未知物料", ncr.getItemId()));
|
}
|
|
String workOrderCode = autoCodeRecordService.generateAutoCode(
|
MesMdAutoCodeRuleCodeEnum.PRO_WORK_ORDER_CODE.getCode());
|
String workOrderName = String.format("%s单-%s", dispositionName, ncr.getCode());
|
MesProWorkOrderSaveReqVO workOrderReqVO = new MesProWorkOrderSaveReqVO();
|
workOrderReqVO.setCode(workOrderCode);
|
workOrderReqVO.setName(workOrderName);
|
workOrderReqVO.setType(MesProWorkOrderTypeEnum.SELF.getType());
|
workOrderReqVO.setOrderSourceType(MesProWorkOrderSourceTypeEnum.STORE.getType());
|
workOrderReqVO.setOrderSourceCode(ncr.getCode());
|
workOrderReqVO.setProductId(product.getId());
|
workOrderReqVO.setQuantity(ncr.getUnqualifiedQuantity());
|
workOrderReqVO.setRequestDate(LocalDateTime.now());
|
workOrderReqVO.setRemark(String.format("由不合格品处理单[%s]处置为[%s]自动生成,请补充工艺路线后下达",
|
ncr.getCode(), dispositionName));
|
// 出货检验来源的 NCR:带上客户,满足产品批次配置(clientFlag)对客户必填的要求
|
Long clientId = resolveSourceClientId(ncr);
|
if (clientId != null) {
|
workOrderReqVO.setClientId(clientId);
|
}
|
Long workOrderId = workOrderService.createWorkOrder(workOrderReqVO);
|
|
MesQcNcrDisposeRespVO respVO = new MesQcNcrDisposeRespVO();
|
respVO.setDisposition(reqVO.getDisposition());
|
respVO.setDocType("WORK_ORDER");
|
respVO.setDocId(workOrderId);
|
respVO.setDocCode(workOrderCode);
|
respVO.setDocName(workOrderName);
|
respVO.setMessage(String.format("已生成%s[%s],请到生产订单中补充工艺路线后下达", workOrderName, workOrderCode));
|
return respVO;
|
}
|
|
/**
|
* 取来源检验单上的客户:仅出货检验(OQC)带有客户信息
|
*/
|
private Long resolveSourceClientId(MesQcNcrDO ncr) {
|
if (!Objects.equals(MesQcTypeEnum.OQC.getType(), ncr.getQcType()) || ncr.getQcId() == null) {
|
return null;
|
}
|
MesQcOqcDO oqc = oqcService.getOqc(ncr.getQcId());
|
return oqc != null ? oqc.getClientId() : null;
|
}
|
|
/**
|
* 物料展示:编码 + 名称
|
*/
|
private String buildItemLabel(MesMdmItemDO item) {
|
if (item == null) {
|
return "未知物料";
|
}
|
String code = StrUtil.blankToDefault(item.getCode(), "");
|
String name = StrUtil.blankToDefault(item.getName(), "");
|
String label = (code + " " + name).trim();
|
return label.isEmpty() ? "ID=" + item.getId() : label;
|
}
|
|
/**
|
* 仓库名称,解析失败回退为可读 ID
|
*/
|
private String resolveWarehouseName(Long warehouseId) {
|
if (warehouseId == null) {
|
return "未选择";
|
}
|
MesWmWarehouseDO warehouse = warehouseService.getWarehouse(warehouseId);
|
return warehouse != null && StrUtil.isNotBlank(warehouse.getName())
|
? warehouse.getName() : "ID=" + warehouseId;
|
}
|
|
/**
|
* 库区名称,解析失败回退为可读 ID
|
*/
|
private String resolveLocationName(Long locationId) {
|
if (locationId == null) {
|
return "未选择";
|
}
|
MesWmWarehouseLocationDO location = warehouseLocationService.getWarehouseLocation(locationId);
|
return location != null && StrUtil.isNotBlank(location.getName())
|
? location.getName() : "ID=" + locationId;
|
}
|
|
/**
|
* 库位名称,解析失败回退为可读 ID
|
*/
|
private String resolveAreaName(Long areaId) {
|
if (areaId == null) {
|
return "未选择";
|
}
|
MesWmWarehouseAreaDO area = warehouseAreaService.getWarehouseArea(areaId);
|
return area != null && StrUtil.isNotBlank(area.getName())
|
? area.getName() : "ID=" + areaId;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void submitNcrApproval(Long id) {
|
// 说明:BPM 的 createProcessInstance 内部通过 FlowableUtils.executeAuthenticatedUserId 执行,
|
// 该方法会把所有异常统一包成 RuntimeException,因此这里不能直接 catch ServiceException,
|
// 必须沿 cause 链判断是否为「流程定义不存在」。
|
// 1. 校验存在 + 待评审(草稿/审批驳回后)状态
|
MesQcNcrDO ncr = validateNcrExists(id);
|
if (!MesNcrStatusEnum.PENDING_REVIEW.getStatus().equals(ncr.getStatus())) {
|
throw exception(QC_NCR_SUBMIT_STATUS_INVALID, ncr.getCode(), resolveStatusName(ncr.getStatus()));
|
}
|
|
// 2. 全局免审批:业务校验通过后直接视为审批通过(已评审/待处置),无需流程模型
|
if (processInstanceApi.isGlobalAutoApprove()) {
|
ncrMapper.updateById(new MesQcNcrDO()
|
.setId(id)
|
.setStatus(MesNcrStatusEnum.REVIEWED.getStatus())
|
.setProcessInstanceId(""));
|
return;
|
}
|
|
// 3. 创建 BPM 流程实例;取不到流程定义时按约定走自动通过兜底
|
String processInstanceId;
|
try {
|
processInstanceId = processInstanceApi.createProcessInstance(getLoginUserId(),
|
new BpmProcessInstanceCreateReqDTO()
|
.setProcessDefinitionKey(NCR_APPROVAL_PROCESS_KEY)
|
.setBusinessKey(String.valueOf(id)));
|
} catch (RuntimeException ex) {
|
if (isProcessDefinitionNotExists(ex)) {
|
ncrMapper.updateById(new MesQcNcrDO()
|
.setId(id)
|
.setStatus(MesNcrStatusEnum.REVIEWED.getStatus())
|
.setProcessInstanceId(""));
|
return;
|
}
|
throw ex;
|
}
|
|
// 4. 更新 NCR 状态为审批中,并回写流程实例编号
|
ncrMapper.updateById(new MesQcNcrDO()
|
.setId(id)
|
.setStatus(MesNcrStatusEnum.APPROVING.getStatus())
|
.setProcessInstanceId(processInstanceId));
|
}
|
|
/**
|
* 判断异常链中是否包含「流程定义不存在」(BPM 对外会包一层 RuntimeException)
|
*/
|
private static boolean isProcessDefinitionNotExists(Throwable ex) {
|
Throwable current = ex;
|
while (current != null) {
|
if (current instanceof ServiceException serviceException
|
&& cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_NOT_EXISTS
|
.getCode().equals(serviceException.getCode())) {
|
return true;
|
}
|
current = current.getCause();
|
}
|
return false;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void updateNcrAuditStatus(Long id, Integer bpmResult) {
|
// 1. 校验存在 + 审批中(避免并发或重复回调覆盖已处置/已关闭状态)
|
MesQcNcrDO ncr = validateNcrExists(id);
|
if (!MesNcrStatusEnum.APPROVING.getStatus().equals(ncr.getStatus())) {
|
return;
|
}
|
|
// 2. 审批通过 -> 已评审(待处置);驳回/取消 -> 待评审(可重新修改并提交)
|
if (BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
|
ncrMapper.updateById(new MesQcNcrDO()
|
.setId(id)
|
.setStatus(MesNcrStatusEnum.REVIEWED.getStatus()));
|
return;
|
}
|
if (BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(bpmResult)
|
|| BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
|
// 保留 processInstanceId:驳回/取消后仍可在「查看审批」中查看审批意见,
|
// 重新提交时会用新流程实例编号覆盖。
|
ncrMapper.updateById(new MesQcNcrDO()
|
.setId(id)
|
.setStatus(MesNcrStatusEnum.PENDING_REVIEW.getStatus()));
|
}
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void closeNcr(Long id, String closeOpinion) {
|
// 1. 校验存在 + 已处置状态
|
MesQcNcrDO ncr = validateNcrExists(id);
|
if (!MesNcrStatusEnum.DISPOSED.getStatus().equals(ncr.getStatus())) {
|
throw exception(QC_NCR_CLOSE_STATUS_INVALID, ncr.getCode(), resolveStatusName(ncr.getStatus()));
|
}
|
|
// 2. 更新关闭信息
|
ncrMapper.updateById(new MesQcNcrDO()
|
.setId(id)
|
.setCloseUserId(getLoginUserId())
|
.setCloseDate(LocalDateTime.now())
|
.setCloseOpinion(closeOpinion)
|
.setStatus(MesNcrStatusEnum.CLOSED.getStatus()));
|
}
|
|
// ==================== 校验方法 ====================
|
|
@Override
|
public MesQcNcrDO validateNcrExists(Long id) {
|
MesQcNcrDO ncr = ncrMapper.selectById(id);
|
if (ncr == null) {
|
throw exception(QC_NCR_NOT_EXISTS, id);
|
}
|
return ncr;
|
}
|
|
/**
|
* 校验 NCR 存在且为待评审状态
|
*/
|
private MesQcNcrDO validateNcrStatusPendingReview(Long id) {
|
MesQcNcrDO ncr = validateNcrExists(id);
|
if (!MesNcrStatusEnum.PENDING_REVIEW.getStatus().equals(ncr.getStatus())) {
|
throw exception(QC_NCR_STATUS_NOT_PENDING, ncr.getCode(), resolveStatusName(ncr.getStatus()));
|
}
|
return ncr;
|
}
|
|
/**
|
* 校验处置方式:必填 + 必须是合法枚举值
|
*/
|
private void validateDisposition(Integer disposition) {
|
if (disposition == null) {
|
throw exception(QC_NCR_DISPOSITION_REQUIRED);
|
}
|
if (!DISPOSITION_ALLOWED_TYPES.contains(disposition)) {
|
throw exception(QC_NCR_DISPOSITION_INVALID, disposition);
|
}
|
}
|
|
/**
|
* 状态值转名称,解析失败回退为 ID 字符串
|
*/
|
private String resolveStatusName(Integer status) {
|
for (MesNcrStatusEnum value : MesNcrStatusEnum.values()) {
|
if (value.getStatus().equals(status)) {
|
return value.getName();
|
}
|
}
|
return "ID=" + status;
|
}
|
|
/**
|
* 校验 NCR 编号唯一
|
*/
|
private void validateNcrCodeUnique(Long id, String code) {
|
MesQcNcrDO existing = ncrMapper.selectByCode(code);
|
if (existing != null && !existing.getId().equals(id)) {
|
throw exception(QC_NCR_CODE_DUPLICATE, code);
|
}
|
}
|
|
/**
|
* 校验物料存在
|
*/
|
private void validateItemExists(Long itemId) {
|
if (itemId != null) {
|
mdmItemApi.getItem(itemId).getCheckedData();
|
}
|
}
|
|
/**
|
* 根据检验类型和检验单 ID 查询来源检验单编号
|
*/
|
private String lookupQcCode(Integer qcType, Long qcId) {
|
if (Objects.equals(qcType, MesQcTypeEnum.IQC.getType())) {
|
return iqcService.getIqc(qcId).getCode();
|
} else if (Objects.equals(qcType, MesQcTypeEnum.IPQC.getType())) {
|
return ipqcService.getIpqc(qcId).getCode();
|
} else if (Objects.equals(qcType, MesQcTypeEnum.OQC.getType())) {
|
return oqcService.getOqc(qcId).getCode();
|
} else if (Objects.equals(qcType, MesQcTypeEnum.RQC.getType())) {
|
return rqcService.getRqc(qcId).getCode();
|
}
|
return null;
|
}
|
|
// ==================== 查询方法 ====================
|
|
@Override
|
public MesQcNcrDO getNcr(Long id) {
|
return ncrMapper.selectById(id);
|
}
|
|
@Override
|
public PageResult<MesQcNcrDO> getNcrPage(MesQcNcrPageReqVO pageReqVO) {
|
return ncrMapper.selectPage(pageReqVO);
|
}
|
|
@Override
|
public Map<Long, MesQcNcrDO> getNcrMap(Collection<Long> ids) {
|
return convertMap(ncrMapper.selectBatchIds(ids), MesQcNcrDO::getId);
|
}
|
|
}
|