6 天以前 85156554438bb13930c89072ee0b7fa93192a7fe
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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
package cn.iocoder.yudao.module.mes.service.qc.iqc;
 
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.module.mes.controller.admin.qc.iqc.vo.MesQcIqcPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.iqc.vo.MesQcIqcSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.defectrecord.MesQcDefectRecordDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.iqc.MesQcIqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.iqc.MesQcIqcMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcDefectLevelEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.vendor.MesMdVendorService;
import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeService;
import cn.iocoder.yudao.module.mes.service.wm.outsourcereceipt.MesWmOutsourceReceiptService;
import cn.iocoder.yudao.module.erp.api.purchase.ErpSupplierApi;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
 
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
 
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
 
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
 
/**
 * MES 来料检验单(IQC) Service 实现类
 *
 * @author 芋道源码
 */
@Service
@Validated
public class MesQcIqcServiceImpl implements MesQcIqcService {
 
    @Resource
    private MesQcIqcMapper iqcMapper;
 
    @Resource
    private MesQcTemplateItemService templateItemService;
    @Resource
    private MesQcIqcLineService iqcLineService;
    @Resource
    private MesQcDefectRecordService defectRecordService;
    @Resource
    private MesWmArrivalNoticeService arrivalNoticeService;
    @Resource
    private MesWmOutsourceReceiptService outsourceReceiptService;
    @Resource
    private MesMdVendorService vendorService;
    @Resource
    private MesMdItemService itemService;
    @Resource
    @Lazy
    private MesQcIndicatorResultService indicatorResultService;
 
    @Resource
    private AdminUserApi adminUserApi;
 
    @Resource
    private MdmItemApi mdmItemApi;
 
    @Resource
    @Lazy
    private ErpSupplierApi supplierApi;
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createIqc(MesQcIqcSaveReqVO createReqVO) {
        // 1.1 校验数据
        validateIqcSaveData(createReqVO);
        // 1.2 校验来源单据参数,并验证数据存在
        String sourceDocCode = validateAndGetSourceDocCode(
                createReqVO.getSourceDocType(), createReqVO.getSourceDocId(), createReqVO.getSourceLineId());
        // 1.3 通过 itemId + IQC 类型自动查找模板
        MesQcTemplateItemDO templateItem = templateItemService.getRequiredTemplateByItemIdAndType(
                createReqVO.getItemId(), MesQcTypeEnum.IQC.getType());
        Long templateId = templateItem.getTemplateId();
 
        // 2. 插入主表
        MesQcIqcDO iqc = BeanUtils.toBean(createReqVO, MesQcIqcDO.class)
                .setTemplateId(templateId)
                .setCheckQuantity(createReqVO.getQualifiedQuantity().add(createReqVO.getUnqualifiedQuantity()))
                .setSourceDocCode(sourceDocCode)
                .setStatus(MesQcStatusEnum.DRAFT.getStatus());
        iqcMapper.insert(iqc);
 
        // 3. 从模板指标自动生成检验行
        iqcLineService.createLinesFromTemplate(iqc.getId(), templateId);
        return iqc.getId();
    }
 
    @Override
    public void updateIqc(MesQcIqcSaveReqVO updateReqVO) {
        // 1.1 校验存在 + 草稿状态
        validateIqcStatusPrepare(updateReqVO.getId());
        // 1.2 校验数据
        validateIqcSaveData(updateReqVO);
 
        // 2. 更新主表
        MesQcIqcDO updateObj = BeanUtils.toBean(updateReqVO, MesQcIqcDO.class);
        updateObj.setSourceDocType(null).setSourceDocId(null).setSourceLineId(null); // 不允许修改来源单据
        updateObj.setTemplateId(null); // 不允许修改模板
        updateObj.setItemId(null); // 不允许修改物料
        updateObj.setCheckQuantity(updateReqVO.getQualifiedQuantity().add(updateReqVO.getUnqualifiedQuantity()));
        iqcMapper.updateById(updateObj);
    }
 
    private void validateIqcSaveData(MesQcIqcSaveReqVO reqVO) {
        // 校验编号唯一
        validateIqcCodeUnique(reqVO.getId(), reqVO.getCode());
        // 校验供应商存在(优先校验 ERP 供应商,失败则校验 MDM 供应商)
        if (reqVO.getVendorId() != null) {
            try {
                supplierApi.validateSupplier(reqVO.getVendorId());
            } catch (Exception e) {
                // ERP 供应商校验失败,尝试 MDM 供应商
                vendorService.validateVendorExists(reqVO.getVendorId());
            }
        }
        // 校验物料存在(使用 MDM 物料)
        if (reqVO.getItemId() != null) {
            mdmItemApi.getItem(reqVO.getItemId()).getCheckedData();
        }
        // 校验检测人员存在
        adminUserApi.validateUser(reqVO.getInspectorUserId());
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void finishIqc(Long id) {
        // 1.1 校验存在 + 草稿状态
        MesQcIqcDO iqc = validateIqcStatusPrepare(id);
        if (iqc.getCheckResult() == null) {
            throw exception(QC_IQC_CHECK_RESULT_EMPTY);
        }
        // 1.2 校验至少存在一条检测结果
        indicatorResultService.validateIndicatorResultExistsByQcIdAndType(id, MesQcTypeEnum.IQC.getType());
 
        // 2. 更新状态为已完成
        MesQcIqcDO updateObj = new MesQcIqcDO()
                .setId(id).setStatus(MesQcStatusEnum.FINISHED.getStatus());
        iqcMapper.updateById(updateObj);
 
        // 3. 回写来源单据
        writeBackSourceDoc(iqc);
    }
 
    /**
     * 回写来源单据(IQC 完成后)
     *
     * @param iqc 来料检验单
     */
    private void writeBackSourceDoc(MesQcIqcDO iqc) {
        // 校验:sourceDocType 非空时,lineId 和 docId 必须非空,否则为脏数据
        if (iqc.getSourceDocType() == null) {
            return;
        }
        if (ObjectUtil.hasNull(iqc.getSourceLineId(), iqc.getSourceDocId())) {
            throw new IllegalArgumentException(
                    "IQC 单[" + iqc.getId() + "] sourceDocType 非空但 sourceLineId 或 sourceDocId 为空");
        }
 
        // 根据不同的来源单据类型,回写不同的单据
        if (Objects.equals(iqc.getSourceDocType(), MesBizTypeConstants.WM_ARRIVAL_NOTICE)) {
            arrivalNoticeService.updateArrivalNoticeWhenIqcFinish(iqc.getSourceDocId(), iqc.getSourceLineId(),
                    iqc.getId(), iqc.getCheckResult(), iqc.getQualifiedQuantity());
        } else if (Objects.equals(iqc.getSourceDocType(), MesBizTypeConstants.WM_OUTSOURCE_RECPT)) {
            outsourceReceiptService.updateOutsourceReceiptWhenIqcFinish(iqc.getSourceDocId(), iqc.getSourceLineId(),
                    iqc.getId(), iqc.getQualifiedQuantity(), iqc.getUnqualifiedQuantity());
        } else {
            throw new IllegalArgumentException(
                    "IQC 单[" + iqc.getId() + "] sourceDocType=" + iqc.getSourceDocType() + " 无法识别,无法回写来源单据");
        }
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteIqc(Long id) {
        // 1. 校验存在 + 草稿状态
        validateIqcStatusPrepare(id);
 
        // 2.1 删除主表
        iqcMapper.deleteById(id);
        // 2.2 级联删除行
        iqcLineService.deleteListByIqcId(id);
        // 2.3 级联删除缺陷记录
        defectRecordService.deleteListByQcTypeAndQcId(MesQcTypeEnum.IQC.getType(), id);
    }
 
    @Override
    public MesQcIqcDO validateIqcExists(Long id) {
        MesQcIqcDO iqc = iqcMapper.selectById(id);
        if (iqc == null) {
            throw exception(QC_IQC_NOT_EXISTS);
        }
        return iqc;
    }
 
    /**
     * 校验来料检验单存在且为草稿状态
     *
     * @param id 来料检验单 ID
     * @return 来料检验单
     */
    private MesQcIqcDO validateIqcStatusPrepare(Long id) {
        MesQcIqcDO iqc = validateIqcExists(id);
        if (ObjUtil.notEqual(iqc.getStatus(), MesQcStatusEnum.DRAFT.getStatus())) {
            throw exception(QC_IQC_NOT_PREPARE);
        }
        return iqc;
    }
 
    private void validateIqcCodeUnique(Long id, String code) {
        MesQcIqcDO iqc = iqcMapper.selectByCode(code);
        if (iqc == null) {
            return;
        }
        if (ObjUtil.notEqual(iqc.getId(), id)) {
            throw exception(QC_IQC_CODE_DUPLICATE);
        }
    }
 
    /**
     * 校验来源单据参数,并验证数据存在
     *
     * @param sourceDocType 来源单据类型
     * @param sourceDocId 来源单据 ID
     * @param sourceLineId 来源单据行 ID
     */
    private String validateAndGetSourceDocCode(Integer sourceDocType, Long sourceDocId, Long sourceLineId) {
        if (sourceDocType == null) {
            return null;
        }
        if (ObjUtil.hasNull(sourceDocId, sourceLineId)) {
            throw exception(QC_IQC_SOURCE_DOC_PARAMS_MISSING);
        }
        // 根据来源单据类型,校验数据存在且匹配
        if (Objects.equals(sourceDocType, MesBizTypeConstants.WM_ARRIVAL_NOTICE)) {
            arrivalNoticeService.validateArrivalNoticeAndLineExists(sourceDocId, sourceLineId);
            return arrivalNoticeService.getArrivalNotice(sourceDocId).getCode();
        } else if (Objects.equals(sourceDocType, MesBizTypeConstants.WM_OUTSOURCE_RECPT)) {
            outsourceReceiptService.validateOutsourceReceiptAndLineExists(sourceDocId, sourceLineId);
            return outsourceReceiptService.getOutsourceReceipt(sourceDocId).getCode();
        }
        return null;
    }
 
    @Override
    public MesQcIqcDO getIqc(Long id) {
        return iqcMapper.selectById(id);
    }
 
    @Override
    public PageResult<MesQcIqcDO> getIqcPage(MesQcIqcPageReqVO pageReqVO) {
        return iqcMapper.selectPage(pageReqVO);
    }
 
    @Override
    public void recalculateDefectStats(Long iqcId, List<MesQcDefectRecordDO> records) {
        MesQcIqcDO iqc = validateIqcExists(iqcId);
        // 1. 行级缺陷统计
        iqcLineService.recalculateLineDefectStats(iqcId, records);
 
        // 2.1 汇总主表的缺陷数量
        int totalCritical = 0;
        int totalMajor = 0;
        int totalMinor = 0;
        for (MesQcDefectRecordDO record : records) {
            int quantity = ObjUtil.defaultIfNull(record.getQuantity(), 1);
            if (Objects.equals(record.getLevel(), MesQcDefectLevelEnum.CRITICAL.getType())) {
                totalCritical += quantity;
            } else if (Objects.equals(record.getLevel(), MesQcDefectLevelEnum.MAJOR.getType())) {
                totalMajor += quantity;
            } else if (Objects.equals(record.getLevel(), MesQcDefectLevelEnum.MINOR.getType())) {
                totalMinor += quantity;
            } else {
                throw exception(QC_DEFECT_RECORD_LEVEL_UNKNOWN);
            }
        }
        // 2.2 计算缺陷率
        BigDecimal criticalRate = BigDecimal.ZERO;
        BigDecimal majorRate = BigDecimal.ZERO;
        BigDecimal minorRate = BigDecimal.ZERO;
        if (iqc.getCheckQuantity() != null && iqc.getCheckQuantity().compareTo(BigDecimal.ZERO) > 0) {
            BigDecimal checkQty = iqc.getCheckQuantity();
            criticalRate = BigDecimal.valueOf(totalCritical).multiply(BigDecimal.valueOf(100))
                    .divide(checkQty, 2, RoundingMode.HALF_UP);
            majorRate = BigDecimal.valueOf(totalMajor).multiply(BigDecimal.valueOf(100))
                    .divide(checkQty, 2, RoundingMode.HALF_UP);
            minorRate = BigDecimal.valueOf(totalMinor).multiply(BigDecimal.valueOf(100))
                    .divide(checkQty, 2, RoundingMode.HALF_UP);
        }
        // 2.3 更新主表
        MesQcIqcDO updateIqc = new MesQcIqcDO().setId(iqcId)
                .setCriticalQuantity(totalCritical).setMajorQuantity(totalMajor).setMinorQuantity(totalMinor)
                .setCriticalRate(criticalRate).setMajorRate(majorRate).setMinorRate(minorRate);
        iqcMapper.updateById(updateIqc);
    }
 
    @Override
    public Map<Long, MesQcIqcDO> getIqcMap(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyMap();
        }
        List<MesQcIqcDO> list = iqcMapper.selectByIds(ids);
        return convertMap(list, MesQcIqcDO::getId);
    }
 
    @Override
    public Long getIqcCountByVendorId(Long vendorId) {
        return iqcMapper.selectCountByVendorId(vendorId);
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createIqcFromArrivalNotice(Long noticeId, MesWmArrivalNoticeLineDO line, String noticeCode, Long vendorId) {
        // 1. 校验物料是否配置了 IQC 检验模板
        MesQcTemplateItemDO templateItem;
        try {
            templateItem = templateItemService.getRequiredTemplateByItemIdAndType(
                    line.getItemId(), MesQcTypeEnum.IQC.getType());
        } catch (Exception e) {
            // 获取物料信息,提供更明确的错误提示
            String itemInfo = getItemInfo(line.getItemId());
            throw exception(QC_NO_TEMPLATE_FOR_ITEM, itemInfo);
        }
        Long templateId = templateItem.getTemplateId();
 
        // 2. 生成检验单编码
        String code = "IQC" + System.currentTimeMillis();
 
        // 3. 创建 IQC 主表(待检验状态,初始合格数量为 0)
        MesQcIqcDO iqc = new MesQcIqcDO();
        iqc.setCode(code);
        iqc.setName(noticeCode + "-物料检验");
        iqc.setTemplateId(templateId);
        iqc.setSourceDocType(MesBizTypeConstants.WM_ARRIVAL_NOTICE);
        iqc.setSourceDocId(noticeId);
        iqc.setSourceLineId(line.getId());
        iqc.setSourceDocCode(noticeCode);
        iqc.setVendorId(vendorId);
        iqc.setItemId(line.getItemId());
        iqc.setReceivedQuantity(line.getArrivalQuantity());
        iqc.setCheckQuantity(BigDecimal.ZERO); // 待填写
        iqc.setQualifiedQuantity(BigDecimal.ZERO); // 待填写
        iqc.setUnqualifiedQuantity(BigDecimal.ZERO); // 待填写
        iqc.setReceiveDate(java.time.LocalDateTime.now());
        iqc.setStatus(MesQcStatusEnum.DRAFT.getStatus());
        iqcMapper.insert(iqc);
 
        // 4. 从模板指标自动生成检验行
        iqcLineService.createLinesFromTemplate(iqc.getId(), templateId);
 
        return iqc.getId();
    }
 
    /**
     * 获取物料信息(编码+名称)
     */
    private String getItemInfo(Long itemId) {
        if (itemId == null) {
            return "未知物料";
        }
        try {
            cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO item = mdmItemApi.getItem(itemId).getCheckedData();
            if (item != null) {
                return item.getCode() + "-" + item.getName();
            }
        } catch (Exception e) {
            // 忽略异常
        }
        return "物料ID:" + itemId;
    }
 
}