云
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
170
171
172
173
174
175
176
package cn.iocoder.yudao.module.mes.service.qc.ncr;
 
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskAutoGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.process.MesProWorkOrderProcessSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrDisposeRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ncr.MesQcNcrDO;
import cn.iocoder.yudao.module.mes.dal.mysql.md.item.MesMdItemMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderBomMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper;
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.dal.mysql.wm.productproduce.MesWmProductProduceLineMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderSourceTypeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderTypeEnum;
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.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Objects;
 
/** 出货或过程不合格重新生产,复制来源工单的工序及 BOM 快照。 */
@Service
public class MesQcNcrOqcReproductionService {
    @Resource private MesQcOqcMapper oqcMapper;
    @Resource private MesWmProductSalesLineMapper salesLineMapper;
    @Resource private MesWmProductSalesMapper salesMapper;
    @Resource private MesWmProductProduceLineMapper produceLineMapper;
    @Resource private MesQcNcrScrapService sourceService;
    @Resource private MesWmBatchMapper batchMapper;
    @Resource private MesProWorkOrderMapper orderMapper;
    @Resource private MesProWorkOrderBomMapper bomMapper;
    @Resource private MesMdItemMapper itemMapper;
    @Resource private MesProWorkOrderService orderService;
    @Resource private MesProWorkOrderProcessService processService;
    @Resource private MesProTaskService taskService;
    @Resource private MesMdAutoCodeRecordService autoCodeService;
 
    @Transactional(rollbackFor = Exception.class)
    public MesQcNcrDisposeRespVO reproduce(MesQcNcrDO ncr, Integer disposition) {
        if (!Objects.equals(ncr.getQcType(), MesQcTypeEnum.OQC.getType()) || ncr.getQcId() == null
                || ncr.getUnqualifiedQuantity() == null || ncr.getUnqualifiedQuantity().signum() <= 0)
            throw error("重新生产需要关联出货质检及有效不合格数量");
        var qc = oqcMapper.selectById(ncr.getQcId());
        if (qc == null || !Objects.equals(qc.getStatus(), MesQcStatusEnum.FINISHED.getStatus())
                || !Objects.equals(qc.getSourceDocType(), MesBizTypeConstants.WM_PRODUCT_SALES)
                || qc.getSourceDocId() == null || qc.getSourceLineId() == null
                || !Objects.equals(qc.getItemId(), ncr.getItemId()) || !same(qc.getUnqualifiedQuantity(), ncr.getUnqualifiedQuantity()))
            throw error("出货检验来源、物料或不合格数量不一致,无法重新生产");
        var line = salesLineMapper.selectById(qc.getSourceLineId());
        var sales = salesMapper.selectById(qc.getSourceDocId());
        if (line == null || sales == null || !Objects.equals(line.getSalesId(), sales.getId())
                || !Objects.equals(line.getOqcId(), qc.getId()) || !Objects.equals(line.getItemId(), ncr.getItemId())
                || !Objects.equals(line.getQualityStatus(), MesWmQualityStatusEnum.FAIL.getStatus())
                || !same(line.getQuantity(), qc.getOutQuantity()) || ncr.getUnqualifiedQuantity().compareTo(line.getQuantity()) > 0)
            throw error("无法唯一核对出货检验对应的不合格出货行和数量");
        if (sales.getClientId() == null || !Objects.equals(sales.getClientId(), qc.getClientId())
                || line.getBatchId() == null || StrUtil.isBlank(line.getBatchCode())
                || !Objects.equals(line.getBatchCode(), qc.getBatchCode()))
            throw error("出货检验缺少一致的客户或批次,无法确定原生产订单");
        var batch = batchMapper.selectById(line.getBatchId());
        if (batch == null || !Objects.equals(batch.getCode(), line.getBatchCode())
                || !Objects.equals(batch.getItemId(), ncr.getItemId()) || !Objects.equals(batch.getClientId(), sales.getClientId())
                || batch.getWorkOrderId() == null || (StrUtil.isNotBlank(qc.getReelNo()) && !Objects.equals(qc.getReelNo(), batch.getReelNo())))
            throw error("出货批次未绑定一致的客户、物料、盘号和原生产订单");
        var original = orderMapper.selectById(batch.getWorkOrderId());
        if (original == null || !Objects.equals(original.getClientId(), sales.getClientId()) || original.getProductId() == null)
            throw error("批次绑定的原生产订单不存在或客户不一致");
        return createFromOriginal(ncr, disposition, original, sales.getClientId(), batch.getCode(), "重新生产-");
    }
 
    /** 调用前须在同一事务内通过 claimIpqcSource 锁定并核销该不合格产出。 */
    @Transactional(rollbackFor = Exception.class)
    public MesQcNcrDisposeRespVO reproduceIpqc(MesQcNcrDO ncr, Integer disposition,
                                               MesQcNcrScrapService.IpqcSource source) {
        var verified = sourceService.validateIpqcSource(ncr);
        if (source == null || !Objects.equals(source, verified)) throw error("过程检验来源产出已变化,请刷新后重试");
        var line = produceLineMapper.selectById(source.sourceLineId());
        if (line == null || ncr.getId() == null || !Objects.equals(line.getNcrDispositionId(), ncr.getId()))
            throw error("过程不合格产出尚未由该处理单核销");
        var original = orderMapper.selectById(source.orderId());
        if (original == null || original.getProductId() == null) throw error("过程检验来源生产订单不存在");
        return createFromOriginal(ncr, disposition, original, original.getClientId(), source.batchCode(), "重新生产-");
    }
 
    /**
     * 出货不合格返工/返修:复用原工单的工艺路线与 BOM 克隆一张返工工单。
     *
     * <p>调用方须已完成来源认领、原成品库存出库及原销售出库单取消。</p>
     */
    @Transactional(rollbackFor = Exception.class)
    public MesQcNcrDisposeRespVO createReworkFromOriginal(MesQcNcrDO ncr, Integer disposition,
                                                          MesProWorkOrderDO original, Long clientId,
                                                          String batchCode, String namePrefix) {
        return createFromOriginal(ncr, disposition, original, clientId, batchCode, namePrefix);
    }
 
    private MesQcNcrDisposeRespVO createFromOriginal(MesQcNcrDO ncr, Integer disposition,
                                                     MesProWorkOrderDO original, Long clientId, String batchCode,
                                                     String namePrefix) {
        var product = itemMapper.selectById(original.getProductId());
        if (product == null || !Objects.equals(product.getMdmItemId(), ncr.getItemId()))
            throw error("原生产订单产品与不合格物料映射不一致");
        var processes = processService.getWorkOrderProcessListByWorkOrderId(original.getId());
        if (processes == null || processes.isEmpty()) throw error("原生产订单没有工序快照,无法自动下达重新生产");
        if (original.getRouteId() == null) throw error("原生产订单未记录工艺路线,无法自动生成生产任务");
        var sourceBoms = bomMapper.selectListByWorkOrderId(original.getId());
        if (sourceBoms == null) throw error("无法读取原工单投料 BOM");
        if (sourceBoms.stream().anyMatch(b -> b.getItemId() == null || b.getQuantity() == null || b.getQuantity().signum() < 0
                || (b.getProcessId() == null && (original.getQuantity() == null || original.getQuantity().signum() <= 0))))
            throw error("原工单 BOM 或订单数量无效,无法复制投料");
        if (sourceBoms.stream().anyMatch(b -> b.getProcessId() != null && processes.stream()
                .noneMatch(p -> Objects.equals(p.getProcessId(), b.getProcessId()))))
            throw error("原工单 BOM 关联了不存在的工序,无法自动复制");
 
        String code = autoCodeService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_WORK_ORDER_CODE.getCode());
        var request = new MesProWorkOrderSaveReqVO();
        request.setCode(code); request.setName(namePrefix + ncr.getCode()); request.setType(MesProWorkOrderTypeEnum.SELF.getType());
        request.setOrderSourceType(MesProWorkOrderSourceTypeEnum.STORE.getType()); request.setOrderSourceCode(ncr.getCode());
        request.setProductId(original.getProductId()); request.setReelNo(original.getReelNo());
        request.setClientId(clientId); request.setQuantity(ncr.getUnqualifiedQuantity());
        request.setRequestDate(LocalDateTime.now()); request.setFeedbackApprove(original.getFeedbackApprove());
        request.setProcessDefinitionKey(original.getProcessDefinitionKey()); request.setFeedbackRole(original.getFeedbackRole());
        request.setRemark("由不合格单[" + ncr.getCode() + "]" + StrUtil.removeSuffix(namePrefix, "-")
                + ",原工单[" + original.getCode() + "],原批次[" + StrUtil.blankToDefault(batchCode, "无") + "]");
        Long id = orderService.createWorkOrder(request);
        orderMapper.updateById(new MesProWorkOrderDO().setId(id).setRouteId(original.getRouteId()).setAutoFinishFlag(original.getAutoFinishFlag()));
        for (var process : processes) {
            var copy = BeanUtils.toBean(process, MesProWorkOrderProcessSaveReqVO.class);
            copy.setId(null); copy.setWorkOrderId(id);
            if (StrUtil.isNotBlank(process.getOutputItemId())) {
                try { copy.setOutputItemIds(Arrays.stream(process.getOutputItemId().split(",")).map(String::trim).map(Long::valueOf).toList()); }
                catch (NumberFormatException ex) { throw error("原工单工序产出物料编号格式错误,无法复制"); }
            }
            processService.createWorkOrderProcess(copy);
        }
        // 工序 BOM 是单位用量,普通工单 BOM 是订单总用量;均使用原工单快照。
        bomMapper.deleteByWorkOrderId(id);
        for (var bom : sourceBoms) {
            BigDecimal amount = bom.getProcessId() != null ? bom.getQuantity()
                    : bom.getQuantity().multiply(ncr.getUnqualifiedQuantity()).divide(original.getQuantity(), 8, RoundingMode.CEILING);
            bomMapper.insert(new MesProWorkOrderBomDO().setWorkOrderId(id).setProcessId(bom.getProcessId())
                    .setItemId(bom.getItemId()).setQuantity(amount).setRemark(bom.getRemark()));
        }
        orderService.confirmWorkOrder(id);
        var taskReq = new MesProTaskAutoGenerateReqVO();
        taskReq.setWorkOrderId(id); taskReq.setRouteId(original.getRouteId());
        var taskIds = taskService.autoGenerateTasks(taskReq);
        if (taskIds == null || taskIds.isEmpty()) throw error("重新生产工单未生成生产任务,无法下达");
        var result = new MesQcNcrDisposeRespVO();
        result.setDisposition(disposition); result.setDocType("WORK_ORDER"); result.setDocId(id);
        result.setDocCode(code); result.setDocName(request.getName()); result.setMessage("已确认重新生产订单[" + code + "],生成 " + taskIds.size() + " 条生产任务草稿,待排程");
        return result;
    }
    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); }
}