| | |
| | | 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.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; |
| | |
| | | @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) |
| | |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void disposeNcr(Long id, Integer disposition, Long handlerUserId, String handleResult) { |
| | | public MesQcNcrDisposeRespVO disposeNcr(MesQcNcrDisposeReqVO disposeReqVO) { |
| | | // 1. 校验存在 + 已评审(审批通过)状态 |
| | | MesQcNcrDO ncr = validateNcrExists(id); |
| | | 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(disposition); |
| | | validateDisposition(disposeReqVO.getDisposition()); |
| | | // 3. 不合格数量必须大于 0,否则无法生成入库单 / 生产订单 |
| | | if (ncr.getUnqualifiedQuantity() == null |
| | | || ncr.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw exception(QC_NCR_DISPOSE_QUANTITY_INVALID, ncr.getUnqualifiedQuantity()); |
| | | } |
| | | |
| | | // 3. 更新处置信息(处置方式以本次处置选择为准并落库) |
| | | // TODO: WM 模块集成 — 处置为退货时触发供应商退货,处置为报废时触发杂项出库 |
| | | // 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(id) |
| | | .setDisposition(disposition) |
| | | .setHandlerUserId(handlerUserId) |
| | | .setId(ncr.getId()) |
| | | .setDisposition(disposeReqVO.getDisposition()) |
| | | .setHandlerUserId(disposeReqVO.getHandlerUserId()) |
| | | .setHandleDate(LocalDateTime.now()) |
| | | .setHandleResult(handleResult) |
| | | .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 |