package cn.iocoder.yudao.module.mes.service.qc.link; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO; import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesLineDO; import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper; import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper; import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcMapper; import cn.iocoder.yudao.module.mes.dal.mysql.wm.productproduce.MesWmProductProduceLineMapper; 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.enums.MesBizTypeConstants; import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum; import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductSalesStatusEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum; import cn.iocoder.yudao.module.mes.service.pro.feedback.MesProFeedbackService; import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesLineService; import jakarta.annotation.Resource; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.util.*; /** 一张已完成质检单对应一条数量完全一致的待检业务行。 */ @Service public class MesQcSourceLinkService { @Resource private MesQcIpqcMapper ipqcMapper; @Resource private MesQcOqcMapper oqcMapper; @Resource private MesProFeedbackMapper feedbackMapper; @Resource private MesWmProductProduceLineMapper produceLineMapper; @Resource private MesWmProductSalesMapper salesMapper; @Resource private MesWmProductSalesLineMapper salesLineMapper; @Resource private ErpSaleOrderApi saleOrderApi; @Resource @Lazy private MesProFeedbackService feedbackService; @Resource @Lazy private MesWmProductSalesLineService salesLineService; public record Candidate(Long lineId, Long documentId, String documentCode, String reelNo, BigDecimal quantity) {} public List feedbackCandidates(Long qcId) { MesQcIpqcDO qc = ipqcMapper.selectById(qcId); requireIpqc(qc); var lines = produceLineMapper.selectList(new LambdaQueryWrapperX() .eq(MesWmProductProduceLineDO::getItemId, qc.getItemId()) .eq(MesWmProductProduceLineDO::getQualityStatus, MesWmQualityStatusEnum.PENDING.getStatus()) .isNotNull(MesWmProductProduceLineDO::getFeedbackId).orderByDesc(MesWmProductProduceLineDO::getId)); List result = new ArrayList<>(); for (var line : lines) { MesProFeedbackDO feedback = feedbackMapper.selectById(line.getFeedbackId()); if (feedback != null && feedbackMismatch(qc, line, feedback) == null) { result.add(new Candidate(line.getId(), feedback.getId(), feedback.getCode(), line.getReelNo(), line.getQuantity())); } } return result; } public List salesCandidates(Long qcId) { MesQcOqcDO qc = oqcMapper.selectById(qcId); requireOqc(qc); var lines = salesLineMapper.selectList(new LambdaQueryWrapperX() .eq(MesWmProductSalesLineDO::getItemId, qc.getItemId()) .eq(MesWmProductSalesLineDO::getOqcCheckFlag, true) .isNull(MesWmProductSalesLineDO::getOqcId).orderByDesc(MesWmProductSalesLineDO::getId)); List result = new ArrayList<>(); for (var line : lines) { var sales = salesMapper.selectById(line.getSalesId()); String reel = salesReel(line); if (sales != null && salesMismatch(qc, line, sales, reel) == null) { result.add(new Candidate(line.getId(), sales.getId(), sales.getCode(), reel, line.getQuantity())); } } return result; } @Transactional(rollbackFor = Exception.class) public void linkFeedback(Long qcId, Long lineId) { var qc = ipqcMapper.selectOne(new LambdaQueryWrapperX().eq(MesQcIpqcDO::getId, qcId).last("FOR UPDATE")); requireIpqc(qc); var line = produceLineMapper.selectById(lineId); if (line == null || line.getFeedbackId() == null) { throw new ServiceException(400, "所选报工产出行不存在或未关联报工单,请刷新候选列表后重试"); } var feedback = feedbackMapper.selectByIdForUpdate(line.getFeedbackId()); line = produceLineMapper.selectOne(new LambdaQueryWrapperX() .eq(MesWmProductProduceLineDO::getId, lineId).last("FOR UPDATE")); String mismatch = feedbackMismatch(qc, line, feedback); if (mismatch != null) throw new ServiceException(400, mismatch); long used = ipqcMapper.selectCount(new LambdaQueryWrapperX() .eq(MesQcIpqcDO::getSourceDocType, MesBizTypeConstants.PRO_FEEDBACK) .eq(MesQcIpqcDO::getSourceLineId, lineId)); if (used > 0) throw new ServiceException(400, "该待检产出行已被其他过程检验单关联,请刷新候选列表后重试"); ipqcMapper.updateById(new MesQcIpqcDO().setId(qcId).setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK) .setSourceDocId(feedback.getId()).setSourceLineId(lineId).setSourceDocCode(feedback.getCode())); feedbackService.updateProFeedbackWhenIpqcFinish(feedback.getId(), lineId, qc.getQualifiedQuantity(), qc.getUnqualifiedQuantity(), zero(qc.getLaborScrapQuantity()), zero(qc.getMaterialScrapQuantity()), zero(qc.getOtherScrapQuantity())); } @Transactional(rollbackFor = Exception.class) public void linkSales(Long qcId, Long lineId) { var qc = oqcMapper.selectOne(new LambdaQueryWrapperX().eq(MesQcOqcDO::getId, qcId).last("FOR UPDATE")); requireOqc(qc); var line = salesLineMapper.selectById(lineId); if (line == null || line.getSalesId() == null) { throw new ServiceException(400, "所选销售出库明细不存在或未关联出库单,请刷新候选列表后重试"); } var sales = salesMapper.selectOne(new LambdaQueryWrapperX() .eq(MesWmProductSalesDO::getId, line.getSalesId()).last("FOR UPDATE")); line = salesLineMapper.selectOne(new LambdaQueryWrapperX() .eq(MesWmProductSalesLineDO::getId, lineId).last("FOR UPDATE")); String mismatch = salesMismatch(qc, line, sales, salesReel(line)); if (mismatch != null) throw new ServiceException(400, mismatch); var linked = oqcMapper.selectOne(new LambdaQueryWrapperX() .eq(MesQcOqcDO::getSourceDocType, MesBizTypeConstants.WM_PRODUCT_SALES) .eq(MesQcOqcDO::getSourceLineId, lineId).last("LIMIT 1")); if (linked != null) { throw new ServiceException(400, "该出库明细已关联出货检验单[" + linked.getCode() + "],请刷新候选列表后重试"); } oqcMapper.updateById(new MesQcOqcDO().setId(qcId).setSourceDocType(MesBizTypeConstants.WM_PRODUCT_SALES) .setSourceDocId(sales.getId()).setSourceLineId(lineId).setSourceDocCode(sales.getCode())); salesLineService.updateProductSalesLineWhenOqcFinish(lineId, qcId, qc.getCheckResult()); } /** 返回第一处不满足的原因;为空表示可关联。 */ private String feedbackMismatch(MesQcIpqcDO q, MesWmProductProduceLineDO l, MesProFeedbackDO f) { if (l == null) return "所选报工产出行不存在或已删除,请刷新候选列表后重试"; if (f == null) return "关联的报工单不存在或已删除,请刷新候选列表后重试"; if (!Objects.equals(f.getStatus(), MesProFeedbackStatusEnum.UNCHECK.getStatus())) { return "报工单[" + f.getCode() + "]当前状态为[" + feedbackStatusName(f.getStatus()) + "],仅待检验报工可关联,请重新选择"; } if (!Objects.equals(l.getQualityStatus(), MesWmQualityStatusEnum.PENDING.getStatus())) { return "报工产出行当前质量状态为[" + qualityStatusName(l.getQualityStatus()) + "],已被检验或已入库,请重新选择"; } if (!Objects.equals(q.getItemId(), l.getItemId())) { return "物料不一致:质检单物料ID=" + display(q.getItemId()) + ",报工产出行物料ID=" + display(l.getItemId()) + ",请重新选择"; } if (!Objects.equals(q.getWorkOrderId(), f.getWorkOrderId())) { return "生产工单不一致:质检单工单ID=" + display(q.getWorkOrderId()) + ",报工单工单ID=" + display(f.getWorkOrderId()) + ",请重新选择"; } if (q.getTaskId() != null && !Objects.equals(q.getTaskId(), f.getTaskId())) { return "生产任务不一致:质检单任务ID=" + display(q.getTaskId()) + ",报工单任务ID=" + display(f.getTaskId()) + ",请重新选择"; } if (!sameReel(q.getReelNo(), l.getReelNo())) { return "盘号不一致:质检单盘号[" + display(q.getReelNo()) + "],报工产出行盘号[" + display(l.getReelNo()) + "],请重新选择"; } if (!sameQty(q.getCheckQuantity(), l.getQuantity())) { return "数量不一致:质检单检测数量[" + display(q.getCheckQuantity()) + "],待检产出行数量[" + display(l.getQuantity()) + "],请重新选择"; } return null; } private String salesMismatch(MesQcOqcDO q, MesWmProductSalesLineDO l, MesWmProductSalesDO s, String reel) { if (l == null) return "所选销售出库明细不存在或已删除,请刷新候选列表后重试"; if (s == null) return "所属销售出库单不存在或已删除,请刷新候选列表后重试"; if (!Objects.equals(s.getStatus(), MesWmProductSalesStatusEnum.CONFIRMED.getStatus())) { return "销售出库单[" + s.getCode() + "]当前状态为[" + salesStatusName(s.getStatus()) + "],仅待检测出库单可关联,请重新选择"; } if (!Boolean.TRUE.equals(l.getOqcCheckFlag())) { return "该出库明细未勾选「需要出货检验」,无法关联,请先确认出库明细设置"; } if (l.getOqcId() != null) { return "该出库明细已关联质检单ID=" + l.getOqcId() + ",请刷新候选列表后重新选择"; } if (!Objects.equals(q.getItemId(), l.getItemId())) { return "物料不一致:质检单物料ID=" + display(q.getItemId()) + ",出库明细物料ID=" + display(l.getItemId()) + ",请重新选择"; } if (q.getClientId() != null && !Objects.equals(q.getClientId(), s.getClientId())) { return "客户不一致:质检单客户ID=" + display(q.getClientId()) + ",销售出库单客户ID=" + display(s.getClientId()) + ",请重新选择"; } if (!sameReel(q.getReelNo(), reel)) { return "盘号不一致:质检单盘号[" + display(q.getReelNo()) + "],出库明细盘号[" + display(reel) + "],请确认销售订单明细盘号后重新选择"; } if (!sameQty(q.getCheckQuantity(), l.getQuantity())) { return "数量不一致:质检单检测数量[" + display(q.getCheckQuantity()) + "],待检出库明细数量[" + display(l.getQuantity()) + "],请重新选择"; } return null; } private String salesReel(MesWmProductSalesLineDO line) { if (line == null || line.getSaleOrderItemId() == null) return null; var item = saleOrderApi.getSaleOrderItem(line.getSaleOrderItemId()).getCheckedData(); return item == null ? null : item.getReelNo(); } private static boolean sameReel(String a, String b) { return a != null && b != null && !a.isBlank() && a.trim().equals(b.trim()); } private static boolean sameQty(BigDecimal a, BigDecimal b) { return a != null && b != null && a.signum() > 0 && a.compareTo(b) == 0; } private static BigDecimal zero(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } private static String display(Object value) { return value == null ? "空" : value.toString(); } private static String feedbackStatusName(Integer status) { for (var e : MesProFeedbackStatusEnum.values()) if (Objects.equals(e.getStatus(), status)) return e.getName(); return "未知(" + display(status) + ")"; } private static String qualityStatusName(Integer status) { for (var e : MesWmQualityStatusEnum.values()) if (Objects.equals(e.getStatus(), status)) return e.getName(); return "未知(" + display(status) + ")"; } private static String salesStatusName(Integer status) { for (var e : MesWmProductSalesStatusEnum.values()) if (Objects.equals(e.getStatus(), status)) return e.getName(); return "未知(" + display(status) + ")"; } private static void requireIpqc(MesQcIpqcDO q) { if (q == null) throw new ServiceException(400, "过程检验单不存在或已删除,请刷新后重试"); if (!Objects.equals(q.getStatus(), MesQcStatusEnum.FINISHED.getStatus())) { throw new ServiceException(400, "仅已完成的过程检验单可关联,当前状态为[" + qcStatusName(q.getStatus()) + "]"); } if (q.getSourceDocType() != null || q.getSourceDocId() != null || q.getSourceLineId() != null) { throw new ServiceException(400, "该过程检验单已关联来源单据[" + display(q.getSourceDocCode()) + "],不能重复关联"); } if (q.getReelNo() == null || q.getReelNo().isBlank()) { throw new ServiceException(400, "该过程检验单缺少盘号,无法按盘号筛选待检产出行,请补充盘号后重试"); } } private static void requireOqc(MesQcOqcDO q) { if (q == null) throw new ServiceException(400, "出货检验单不存在或已删除,请刷新后重试"); if (!Objects.equals(q.getStatus(), MesQcStatusEnum.FINISHED.getStatus())) { throw new ServiceException(400, "仅已完成的出货检验单可关联,当前状态为[" + qcStatusName(q.getStatus()) + "]"); } if (q.getSourceDocType() != null || q.getSourceDocId() != null || q.getSourceLineId() != null) { throw new ServiceException(400, "该出货检验单已关联来源单据[" + display(q.getSourceDocCode()) + "],不能重复关联"); } if (q.getReelNo() == null || q.getReelNo().isBlank()) { throw new ServiceException(400, "该出货检验单缺少盘号,无法按盘号筛选待检出库明细,请补充盘号后重试"); } } private static String qcStatusName(Integer status) { for (var e : MesQcStatusEnum.values()) if (Objects.equals(e.getStatus(), status)) return e.getName(); return "未知(" + display(status) + ")"; } }