云
6 天以前 8bcec29715251194f7358bd1035033293e955ce1
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
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) + ")";
    }
}