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<Candidate> feedbackCandidates(Long qcId) {
|
MesQcIpqcDO qc = ipqcMapper.selectById(qcId);
|
requireIpqc(qc);
|
var lines = produceLineMapper.selectList(new LambdaQueryWrapperX<MesWmProductProduceLineDO>()
|
.eq(MesWmProductProduceLineDO::getItemId, qc.getItemId())
|
.eq(MesWmProductProduceLineDO::getQualityStatus, MesWmQualityStatusEnum.PENDING.getStatus())
|
.isNotNull(MesWmProductProduceLineDO::getFeedbackId).orderByDesc(MesWmProductProduceLineDO::getId));
|
List<Candidate> 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<Candidate> salesCandidates(Long qcId) {
|
MesQcOqcDO qc = oqcMapper.selectById(qcId);
|
requireOqc(qc);
|
var lines = salesLineMapper.selectList(new LambdaQueryWrapperX<MesWmProductSalesLineDO>()
|
.eq(MesWmProductSalesLineDO::getItemId, qc.getItemId())
|
.eq(MesWmProductSalesLineDO::getOqcCheckFlag, true)
|
.isNull(MesWmProductSalesLineDO::getOqcId).orderByDesc(MesWmProductSalesLineDO::getId));
|
List<Candidate> 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<MesQcIpqcDO>().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<MesWmProductProduceLineDO>()
|
.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<MesQcIpqcDO>()
|
.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<MesQcOqcDO>().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<MesWmProductSalesDO>()
|
.eq(MesWmProductSalesDO::getId, line.getSalesId()).last("FOR UPDATE"));
|
line = salesLineMapper.selectOne(new LambdaQueryWrapperX<MesWmProductSalesLineDO>()
|
.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<MesQcOqcDO>()
|
.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) + ")";
|
}
|
}
|