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.util.object.BeanUtils;
|
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderSaveReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskAutoGenerateReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.process.MesProWorkOrderProcessSaveReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrDisposeRespVO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ncr.MesQcNcrDO;
|
import cn.iocoder.yudao.module.mes.dal.mysql.md.item.MesMdItemMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderBomMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesLineMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productproduce.MesWmProductProduceLineMapper;
|
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
|
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.MesQcStatusEnum;
|
import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
|
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
|
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
|
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
|
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
|
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
|
import jakarta.annotation.Resource;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import java.math.BigDecimal;
|
import java.math.RoundingMode;
|
import java.time.LocalDateTime;
|
import java.util.Arrays;
|
import java.util.Objects;
|
|
/** 出货或过程不合格重新生产,复制来源工单的工序及 BOM 快照。 */
|
@Service
|
public class MesQcNcrOqcReproductionService {
|
@Resource private MesQcOqcMapper oqcMapper;
|
@Resource private MesWmProductSalesLineMapper salesLineMapper;
|
@Resource private MesWmProductSalesMapper salesMapper;
|
@Resource private MesWmProductProduceLineMapper produceLineMapper;
|
@Resource private MesQcNcrScrapService sourceService;
|
@Resource private MesWmBatchMapper batchMapper;
|
@Resource private MesProWorkOrderMapper orderMapper;
|
@Resource private MesProWorkOrderBomMapper bomMapper;
|
@Resource private MesMdItemMapper itemMapper;
|
@Resource private MesProWorkOrderService orderService;
|
@Resource private MesProWorkOrderProcessService processService;
|
@Resource private MesProTaskService taskService;
|
@Resource private MesMdAutoCodeRecordService autoCodeService;
|
|
@Transactional(rollbackFor = Exception.class)
|
public MesQcNcrDisposeRespVO reproduce(MesQcNcrDO ncr, Integer disposition) {
|
if (!Objects.equals(ncr.getQcType(), MesQcTypeEnum.OQC.getType()) || ncr.getQcId() == null
|
|| ncr.getUnqualifiedQuantity() == null || ncr.getUnqualifiedQuantity().signum() <= 0)
|
throw error("重新生产需要关联出货质检及有效不合格数量");
|
var qc = oqcMapper.selectById(ncr.getQcId());
|
if (qc == null || !Objects.equals(qc.getStatus(), MesQcStatusEnum.FINISHED.getStatus())
|
|| !Objects.equals(qc.getSourceDocType(), MesBizTypeConstants.WM_PRODUCT_SALES)
|
|| qc.getSourceDocId() == null || qc.getSourceLineId() == null
|
|| !Objects.equals(qc.getItemId(), ncr.getItemId()) || !same(qc.getUnqualifiedQuantity(), ncr.getUnqualifiedQuantity()))
|
throw error("出货检验来源、物料或不合格数量不一致,无法重新生产");
|
var line = salesLineMapper.selectById(qc.getSourceLineId());
|
var sales = salesMapper.selectById(qc.getSourceDocId());
|
if (line == null || sales == null || !Objects.equals(line.getSalesId(), sales.getId())
|
|| !Objects.equals(line.getOqcId(), qc.getId()) || !Objects.equals(line.getItemId(), ncr.getItemId())
|
|| !Objects.equals(line.getQualityStatus(), MesWmQualityStatusEnum.FAIL.getStatus())
|
|| !same(line.getQuantity(), qc.getOutQuantity()) || ncr.getUnqualifiedQuantity().compareTo(line.getQuantity()) > 0)
|
throw error("无法唯一核对出货检验对应的不合格出货行和数量");
|
if (sales.getClientId() == null || !Objects.equals(sales.getClientId(), qc.getClientId())
|
|| line.getBatchId() == null || StrUtil.isBlank(line.getBatchCode())
|
|| !Objects.equals(line.getBatchCode(), qc.getBatchCode()))
|
throw error("出货检验缺少一致的客户或批次,无法确定原生产订单");
|
var batch = batchMapper.selectById(line.getBatchId());
|
if (batch == null || !Objects.equals(batch.getCode(), line.getBatchCode())
|
|| !Objects.equals(batch.getItemId(), ncr.getItemId()) || !Objects.equals(batch.getClientId(), sales.getClientId())
|
|| batch.getWorkOrderId() == null || (StrUtil.isNotBlank(qc.getReelNo()) && !Objects.equals(qc.getReelNo(), batch.getReelNo())))
|
throw error("出货批次未绑定一致的客户、物料、盘号和原生产订单");
|
var original = orderMapper.selectById(batch.getWorkOrderId());
|
if (original == null || !Objects.equals(original.getClientId(), sales.getClientId()) || original.getProductId() == null)
|
throw error("批次绑定的原生产订单不存在或客户不一致");
|
return createFromOriginal(ncr, disposition, original, sales.getClientId(), batch.getCode(), "重新生产-");
|
}
|
|
/** 调用前须在同一事务内通过 claimIpqcSource 锁定并核销该不合格产出。 */
|
@Transactional(rollbackFor = Exception.class)
|
public MesQcNcrDisposeRespVO reproduceIpqc(MesQcNcrDO ncr, Integer disposition,
|
MesQcNcrScrapService.IpqcSource source) {
|
var verified = sourceService.validateIpqcSource(ncr);
|
if (source == null || !Objects.equals(source, verified)) throw error("过程检验来源产出已变化,请刷新后重试");
|
var line = produceLineMapper.selectById(source.sourceLineId());
|
if (line == null || ncr.getId() == null || !Objects.equals(line.getNcrDispositionId(), ncr.getId()))
|
throw error("过程不合格产出尚未由该处理单核销");
|
var original = orderMapper.selectById(source.orderId());
|
if (original == null || original.getProductId() == null) throw error("过程检验来源生产订单不存在");
|
return createFromOriginal(ncr, disposition, original, original.getClientId(), source.batchCode(), "重新生产-");
|
}
|
|
/**
|
* 出货不合格返工/返修:复用原工单的工艺路线与 BOM 克隆一张返工工单。
|
*
|
* <p>调用方须已完成来源认领、原成品库存出库及原销售出库单取消。</p>
|
*/
|
@Transactional(rollbackFor = Exception.class)
|
public MesQcNcrDisposeRespVO createReworkFromOriginal(MesQcNcrDO ncr, Integer disposition,
|
MesProWorkOrderDO original, Long clientId,
|
String batchCode, String namePrefix) {
|
return createFromOriginal(ncr, disposition, original, clientId, batchCode, namePrefix);
|
}
|
|
private MesQcNcrDisposeRespVO createFromOriginal(MesQcNcrDO ncr, Integer disposition,
|
MesProWorkOrderDO original, Long clientId, String batchCode,
|
String namePrefix) {
|
var product = itemMapper.selectById(original.getProductId());
|
if (product == null || !Objects.equals(product.getMdmItemId(), ncr.getItemId()))
|
throw error("原生产订单产品与不合格物料映射不一致");
|
var processes = processService.getWorkOrderProcessListByWorkOrderId(original.getId());
|
if (processes == null || processes.isEmpty()) throw error("原生产订单没有工序快照,无法自动下达重新生产");
|
if (original.getRouteId() == null) throw error("原生产订单未记录工艺路线,无法自动生成生产任务");
|
var sourceBoms = bomMapper.selectListByWorkOrderId(original.getId());
|
if (sourceBoms == null) throw error("无法读取原工单投料 BOM");
|
if (sourceBoms.stream().anyMatch(b -> b.getItemId() == null || b.getQuantity() == null || b.getQuantity().signum() < 0
|
|| (b.getProcessId() == null && (original.getQuantity() == null || original.getQuantity().signum() <= 0))))
|
throw error("原工单 BOM 或订单数量无效,无法复制投料");
|
if (sourceBoms.stream().anyMatch(b -> b.getProcessId() != null && processes.stream()
|
.noneMatch(p -> Objects.equals(p.getProcessId(), b.getProcessId()))))
|
throw error("原工单 BOM 关联了不存在的工序,无法自动复制");
|
|
String code = autoCodeService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_WORK_ORDER_CODE.getCode());
|
var request = new MesProWorkOrderSaveReqVO();
|
request.setCode(code); request.setName(namePrefix + ncr.getCode()); request.setType(MesProWorkOrderTypeEnum.SELF.getType());
|
request.setOrderSourceType(MesProWorkOrderSourceTypeEnum.STORE.getType()); request.setOrderSourceCode(ncr.getCode());
|
request.setProductId(original.getProductId()); request.setReelNo(original.getReelNo());
|
request.setClientId(clientId); request.setQuantity(ncr.getUnqualifiedQuantity());
|
request.setRequestDate(LocalDateTime.now()); request.setFeedbackApprove(original.getFeedbackApprove());
|
request.setProcessDefinitionKey(original.getProcessDefinitionKey()); request.setFeedbackRole(original.getFeedbackRole());
|
request.setRemark("由不合格单[" + ncr.getCode() + "]" + StrUtil.removeSuffix(namePrefix, "-")
|
+ ",原工单[" + original.getCode() + "],原批次[" + StrUtil.blankToDefault(batchCode, "无") + "]");
|
Long id = orderService.createWorkOrder(request);
|
orderMapper.updateById(new MesProWorkOrderDO().setId(id).setRouteId(original.getRouteId()).setAutoFinishFlag(original.getAutoFinishFlag()));
|
for (var process : processes) {
|
var copy = BeanUtils.toBean(process, MesProWorkOrderProcessSaveReqVO.class);
|
copy.setId(null); copy.setWorkOrderId(id);
|
if (StrUtil.isNotBlank(process.getOutputItemId())) {
|
try { copy.setOutputItemIds(Arrays.stream(process.getOutputItemId().split(",")).map(String::trim).map(Long::valueOf).toList()); }
|
catch (NumberFormatException ex) { throw error("原工单工序产出物料编号格式错误,无法复制"); }
|
}
|
processService.createWorkOrderProcess(copy);
|
}
|
// 工序 BOM 是单位用量,普通工单 BOM 是订单总用量;均使用原工单快照。
|
bomMapper.deleteByWorkOrderId(id);
|
for (var bom : sourceBoms) {
|
BigDecimal amount = bom.getProcessId() != null ? bom.getQuantity()
|
: bom.getQuantity().multiply(ncr.getUnqualifiedQuantity()).divide(original.getQuantity(), 8, RoundingMode.CEILING);
|
bomMapper.insert(new MesProWorkOrderBomDO().setWorkOrderId(id).setProcessId(bom.getProcessId())
|
.setItemId(bom.getItemId()).setQuantity(amount).setRemark(bom.getRemark()));
|
}
|
orderService.confirmWorkOrder(id);
|
var taskReq = new MesProTaskAutoGenerateReqVO();
|
taskReq.setWorkOrderId(id); taskReq.setRouteId(original.getRouteId());
|
var taskIds = taskService.autoGenerateTasks(taskReq);
|
if (taskIds == null || taskIds.isEmpty()) throw error("重新生产工单未生成生产任务,无法下达");
|
var result = new MesQcNcrDisposeRespVO();
|
result.setDisposition(disposition); result.setDocType("WORK_ORDER"); result.setDocId(id);
|
result.setDocCode(code); result.setDocName(request.getName()); result.setMessage("已确认重新生产订单[" + code + "],生成 " + taskIds.size() + " 条生产任务草稿,待排程");
|
return result;
|
}
|
private static boolean same(BigDecimal a, BigDecimal b) { return a != null && b != null && a.compareTo(b) == 0; }
|
private static ServiceException error(String message) { return new ServiceException(400, message); }
|
}
|