云
4 天以前 f02d0df2f1dfc7a8b775cf50d45375e57628a5ef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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); }
}