package cn.iocoder.yudao.module.mes.service.qc.ncr;
|
|
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
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.dal.dataobject.qc.ncr.MesQcNcrDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transaction.MesWmTransactionDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.scrap.MesWmScrapReceiptDO;
|
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.qc.rqc.MesQcRqcMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productproduce.MesWmProductProduceLineMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productproduce.MesWmProductProduceMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.wm.returnsales.MesWmReturnSalesLineMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.wm.scrap.MesWmScrapReceiptMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.wm.transaction.MesWmTransactionMapper;
|
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
|
import cn.iocoder.yudao.module.mes.enums.qc.MesNcrDispositionEnum;
|
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.enums.wm.MesWmTransactionTypeEnum;
|
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
|
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
|
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
|
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
|
import jakarta.annotation.Resource;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import java.math.BigDecimal;
|
import java.time.LocalDateTime;
|
import java.util.Objects;
|
|
/** 只接收已确认未入库的过程不合格产出或客户退货,不推测其他来源的库存状态。 */
|
@Service
|
public class MesQcNcrScrapService {
|
@Resource private MesQcIpqcMapper ipqcMapper;
|
@Resource private MesQcRqcMapper rqcMapper;
|
@Resource private MesProWorkOrderMapper orderMapper;
|
@Resource private MesWmProductProduceLineMapper produceLineMapper;
|
@Resource private MesWmProductProduceMapper produceMapper;
|
@Resource private MesWmReturnSalesLineMapper returnLineMapper;
|
@Resource private MesWmScrapReceiptMapper scrapMapper;
|
@Resource private MesWmTransactionMapper transactionMapper;
|
@Resource private MesWmTransactionService transactionService;
|
@Resource private MesWmWarehouseService warehouseService;
|
@Resource private MesWmWarehouseAreaService areaService;
|
|
@Transactional(rollbackFor = Exception.class)
|
public MesQcNcrDisposeRespVO receive(MesQcNcrDO ncr, MesQcNcrDisposeReqVO req) {
|
if (ncr.getQcId() == null || ncr.getItemId() == null || ncr.getUnqualifiedQuantity() == null
|
|| ncr.getUnqualifiedQuantity().signum() <= 0) throw error("不合格单来源、物料或数量不完整");
|
Long orderId = null, batchId, sourceLineId;
|
String orderCode = null, batchCode;
|
if (Objects.equals(ncr.getQcType(), MesQcTypeEnum.IPQC.getType())) {
|
var source = validateIpqcSource(ncr);
|
orderId = source.orderId(); orderCode = source.orderCode(); batchId = source.batchId();
|
batchCode = source.batchCode(); sourceLineId = source.sourceLineId();
|
} else if (Objects.equals(ncr.getQcType(), MesQcTypeEnum.RQC.getType())) {
|
var qc = rqcMapper.selectById(ncr.getQcId());
|
if (qc == null || !Objects.equals(qc.getStatus(), MesQcStatusEnum.FINISHED.getStatus())
|
|| !Objects.equals(qc.getSourceDocType(), MesBizTypeConstants.WM_RETURN_SALES) || qc.getSourceDocId() == null
|
|| qc.getSourceLineId() == null || !Objects.equals(qc.getItemId(), ncr.getItemId())
|
|| !same(qc.getUnqualifiedQuantity(), ncr.getUnqualifiedQuantity()))
|
throw error("仅关联客户销售退货的不合格检验可入废料,且物料数量必须一致");
|
var lines = returnLineMapper.selectListByReturnId(qc.getSourceDocId()).stream()
|
.filter(l -> Objects.equals(l.getRqcId(), qc.getId())
|
&& Objects.equals(l.getQualityStatus(), MesWmQualityStatusEnum.FAIL.getStatus())
|
&& Objects.equals(l.getItemId(), ncr.getItemId()) && same(l.getQuantity(), ncr.getUnqualifiedQuantity())
|
&& Objects.equals(l.getBatchCode(), qc.getBatchCode())).toList();
|
if (lines.size() != 1) throw error("无法唯一定位退货检验对应的不合格退货行,请核对关联");
|
var line = lines.get(0);
|
requireNoTransaction(MesBizTypeConstants.WM_RETURN_SALES, qc.getSourceDocId(), line.getId());
|
batchId = line.getBatchId(); batchCode = line.getBatchCode(); sourceLineId = line.getId();
|
} else {
|
throw error("仅过程检验和客户退货检验可以入废料");
|
}
|
if (req.getWarehouseId() == null || req.getLocationId() == null || req.getAreaId() == null)
|
throw error("请选择废料仓、库区和库位");
|
var warehouse = warehouseService.validateWarehouseExists(req.getWarehouseId());
|
if (!Objects.equals("废料仓", warehouse.getWarehouseType()) && !Objects.equals("废料", warehouse.getWarehouseType()))
|
throw error("请选择仓库类型为废料仓的仓库");
|
areaService.validateWarehouseAreaExists(req.getWarehouseId(), req.getLocationId(), req.getAreaId());
|
String code = "SCR-N" + ncr.getQcType() + "-" + sourceLineId;
|
if (scrapMapper.selectByCode(code) != null) throw error("该检验单的不合格品已登记废料,请勿重复处置");
|
var receipt = new MesWmScrapReceiptDO();
|
receipt.setCode(code); receipt.setWorkOrderId(orderId); receipt.setWorkOrderCode(orderCode);
|
receipt.setItemId(ncr.getItemId()); receipt.setQuantity(ncr.getUnqualifiedQuantity());
|
receipt.setWarehouseId(req.getWarehouseId()); receipt.setLocationId(req.getLocationId()); receipt.setAreaId(req.getAreaId());
|
receipt.setBatchId(batchId); receipt.setBatchCode(batchCode); receipt.setReceiptTime(LocalDateTime.now());
|
receipt.setRemark("来源不合格品处理单 " + ncr.getCode());
|
scrapMapper.insert(receipt);
|
Long tx = transactionService.createTransaction(new MesWmTransactionSaveReqDTO()
|
.setType(MesWmTransactionTypeEnum.IN.getType()).setBizType(MesBizTypeConstants.WM_SCRAP_RECEIPT)
|
.setBizId(receipt.getId()).setBizLineId(receipt.getId()).setBizCode(code).setWorkOrderId(orderId)
|
.setItemId(receipt.getItemId()).setQuantity(receipt.getQuantity()).setBatchId(batchId).setBatchCode(batchCode)
|
.setWarehouseId(receipt.getWarehouseId()).setLocationId(receipt.getLocationId()).setAreaId(receipt.getAreaId())
|
.setReceiptTime(receipt.getReceiptTime()).setForceCreateNewStock(true));
|
receipt.setTransactionId(tx); scrapMapper.updateById(receipt);
|
var result = new MesQcNcrDisposeRespVO();
|
result.setDisposition(MesNcrDispositionEnum.SCRAP.getType()); result.setDocType("SCRAP_RECEIPT");
|
result.setDocId(receipt.getId()); result.setDocCode(code); result.setDocName("废料入库单");
|
result.setMessage("已登记废料入库单[" + code + "]并入废料仓"); return result;
|
}
|
|
/** 让步入库与报废必须复用同一不合格产出来源和批次,调用者负责事务及来源核销。 */
|
public IpqcSource validateIpqcSource(MesQcNcrDO ncr) {
|
if (ncr.getQcId() == null || ncr.getItemId() == null || ncr.getUnqualifiedQuantity() == null
|
|| ncr.getUnqualifiedQuantity().signum() <= 0 || !Objects.equals(ncr.getQcType(), MesQcTypeEnum.IPQC.getType()))
|
throw error("不合格单过程检验来源、物料或数量不完整");
|
var qc = ipqcMapper.selectById(ncr.getQcId());
|
if (qc == null || !Objects.equals(qc.getStatus(), MesQcStatusEnum.FINISHED.getStatus())
|
|| !Objects.equals(qc.getSourceDocType(), MesBizTypeConstants.PRO_FEEDBACK)
|
|| qc.getSourceDocId() == null || qc.getSourceLineId() == null
|
|| !Objects.equals(qc.getItemId(), ncr.getItemId()) || !same(qc.getUnqualifiedQuantity(), ncr.getUnqualifiedQuantity()))
|
throw error("过程检验未关联完整报工产出或不合格数量不一致,不能入库");
|
var original = produceLineMapper.selectById(qc.getSourceLineId());
|
if (original == null || original.getProduceId() == null || !Objects.equals(original.getFeedbackId(), qc.getSourceDocId()))
|
throw error("过程检验原产出行不存在或报工不一致");
|
var produce = produceMapper.selectById(original.getProduceId());
|
if (produce == null || !Objects.equals(produce.getFeedbackId(), qc.getSourceDocId())
|
|| qc.getWorkOrderId() == null || !Objects.equals(produce.getWorkOrderId(), qc.getWorkOrderId()))
|
throw error("过程检验、报工产出和生产工单关联不一致");
|
var candidates = produceLineMapper.selectListByFeedbackId(qc.getSourceDocId()).stream()
|
.filter(l -> Objects.equals(l.getProduceId(), original.getProduceId())
|
&& Objects.equals(l.getQualityStatus(), MesWmQualityStatusEnum.FAIL.getStatus())
|
&& Objects.equals(l.getItemId(), ncr.getItemId()) && same(l.getQuantity(), ncr.getUnqualifiedQuantity())
|
&& Objects.equals(l.getBatchId(), original.getBatchId()) && Objects.equals(l.getBatchCode(), original.getBatchCode()))
|
.toList();
|
if (candidates.size() != 1) throw error("无法唯一定位未入库的不合格产出行");
|
var line = candidates.get(0);
|
requireNoTransaction(MesBizTypeConstants.WM_PRODUCT_PRODUCE, line.getProduceId(), line.getId());
|
var order = qc.getWorkOrderId() == null ? null : orderMapper.selectById(qc.getWorkOrderId());
|
if (order == null) throw error("过程检验生产订单不存在");
|
return new IpqcSource(order.getId(), order.getCode(), line.getId(), line.getBatchId(), line.getBatchCode());
|
}
|
|
public record IpqcSource(Long orderId, String orderCode, Long sourceLineId, Long batchId, String batchCode) {}
|
|
/** 条件更新持有行锁直到事务结束,和库存业务一同提交或回滚。 */
|
public void claimIpqcSource(MesQcNcrDO ncr, IpqcSource source) {
|
if (ncr.getId() == null || source == null || source.sourceLineId() == null) throw error("不合格产出来源不完整");
|
LambdaUpdateWrapper<MesWmProductProduceLineDO> claim = new LambdaUpdateWrapper<MesWmProductProduceLineDO>()
|
.set(MesWmProductProduceLineDO::getNcrDispositionId, ncr.getId())
|
.eq(MesWmProductProduceLineDO::getId, source.sourceLineId())
|
.eq(MesWmProductProduceLineDO::getQualityStatus, MesWmQualityStatusEnum.FAIL.getStatus())
|
.eq(MesWmProductProduceLineDO::getItemId, ncr.getItemId())
|
.eq(MesWmProductProduceLineDO::getQuantity, ncr.getUnqualifiedQuantity())
|
.isNull(MesWmProductProduceLineDO::getNcrDispositionId);
|
if (source.batchId() == null) claim.isNull(MesWmProductProduceLineDO::getBatchId);
|
else claim.eq(MesWmProductProduceLineDO::getBatchId, source.batchId());
|
if (source.batchCode() == null) claim.isNull(MesWmProductProduceLineDO::getBatchCode);
|
else claim.eq(MesWmProductProduceLineDO::getBatchCode, source.batchCode());
|
int changed = produceLineMapper.update(null, claim);
|
if (changed != 1) throw error("不合格产出行已处置或发生变化,请刷新后核对");
|
}
|
|
private void requireNoTransaction(Integer type, Long docId, Long lineId) {
|
if (transactionMapper.selectCount(new LambdaQueryWrapperX<MesWmTransactionDO>()
|
.eq(MesWmTransactionDO::getBizType, type).eq(MesWmTransactionDO::getBizId, docId)
|
.eq(MesWmTransactionDO::getBizLineId, lineId)) > 0)
|
throw error("来源不合格行已有库存流水,请先对账,不能重复入库");
|
}
|
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); }
|
}
|