云
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
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.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ncr.MesQcNcrDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcDO;
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.qc.oqc.MesQcOqcMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesLineMapper;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transaction.MesWmTransactionDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.transaction.MesWmTransactionMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrDisposeReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesShippingReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.detail.MesWmProductSalesDetailSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesService;
import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesDetailService;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
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.wm.productsales.MesWmProductSalesLineService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import java.util.Objects;
import java.math.BigDecimal;
import java.util.List;
 
/** 将让步决定回写原出库行;必须由 NCR 处置事务调用。 */
@Component
public class MesQcNcrOqcConcessionService {
    @Resource private MesQcOqcMapper oqcMapper;
    @Resource private MesWmProductSalesMapper salesMapper;
    @Resource private MesWmProductSalesLineMapper lineMapper;
    @Resource private MesWmProductSalesLineService lineService;
    @Resource private MesWmTransactionMapper transactionMapper;
    @Resource private MesWmMaterialStockService stockService;
    @Resource private MesWmProductSalesDetailService detailService;
    @Resource private MesWmProductSalesService salesService;
 
    public void release(MesQcNcrDO ncr, MesQcNcrDisposeReqVO req) {
        MesQcOqcDO qc = oqcMapper.selectById(ncr.getQcId());
        if (qc == null || !Objects.equals(qc.getSourceDocType(), MesBizTypeConstants.WM_PRODUCT_SALES)
                || qc.getSourceDocId() == null || qc.getSourceLineId() == null || StrUtil.isBlank(qc.getBatchCode())
                || !Objects.equals(qc.getStatus(), MesQcStatusEnum.FINISHED.getStatus())
                || !Objects.equals(qc.getCheckResult(), 2) || !Objects.equals(qc.getItemId(), ncr.getItemId())
                || qc.getUnqualifiedQuantity() == null || ncr.getUnqualifiedQuantity() == null
                || qc.getUnqualifiedQuantity().signum() <= 0
                || qc.getUnqualifiedQuantity().compareTo(ncr.getUnqualifiedQuantity()) != 0) {
            throw new ServiceException(400, "出货让步需关联已完成的不合格出货检验、原出库行及明确批次,物料和不合格数量须一致");
        }
        MesWmProductSalesDO sales = salesMapper.selectOne(new LambdaQueryWrapperX<MesWmProductSalesDO>()
                .eq(MesWmProductSalesDO::getId, qc.getSourceDocId()).last("FOR UPDATE"));
        MesWmProductSalesLineDO line = lineMapper.selectOne(new LambdaQueryWrapperX<MesWmProductSalesLineDO>()
                .eq(MesWmProductSalesLineDO::getId, qc.getSourceLineId()).last("FOR UPDATE"));
        if (sales == null || line == null || !Objects.equals(sales.getStatus(), MesWmProductSalesStatusEnum.CONFIRMED.getStatus())
                || !Objects.equals(line.getSalesId(), sales.getId()) || !Objects.equals(line.getOqcId(), qc.getId())
                || !Objects.equals(line.getQualityStatus(), MesWmQualityStatusEnum.FAIL.getStatus())
                || !Objects.equals(line.getItemId(), qc.getItemId()) || line.getQuantity() == null
                || qc.getCheckQuantity() == null || line.getQuantity().signum() <= 0
                || qc.getQualifiedQuantity() == null || qc.getQualifiedQuantity().signum() < 0
                || qc.getQualifiedQuantity().add(qc.getUnqualifiedQuantity()).compareTo(qc.getCheckQuantity()) != 0
                || (StrUtil.isNotBlank(line.getBatchCode()) && !Objects.equals(line.getBatchCode(), qc.getBatchCode()))
                || qc.getCheckQuantity().compareTo(line.getQuantity()) != 0) {
            throw new ServiceException(400, "出货让步要求原出库单处于待检验状态,原行仍绑定本次不合格检验且检测数量覆盖整行");
        }
        MesWmMaterialStockDO stock = req.getMaterialStockId() == null ? null : stockService.getMaterialStock(req.getMaterialStockId());
        if (stock == null || !Objects.equals(stock.getItemId(), qc.getItemId())
                || !Objects.equals(stock.getBatchCode(), qc.getBatchCode())
                || (line.getMaterialStockId() != null && !Objects.equals(line.getMaterialStockId(), stock.getId()))
                || stock.getWarehouseId() == null || stock.getLocationId() == null || stock.getAreaId() == null) {
            throw new ServiceException(400, "请选择原检验批次的实际库存及完整库位;原行指定库存时必须选择该库存");
        }
        var currentDetails = detailService.getProductSalesDetailListByLineId(line.getId());
        if (!currentDetails.isEmpty()) {
            throw new ServiceException(400, "该出库行已有拣货明细,请先核对并清理后重新选择实际库存");
        }
        // 其他行也必须完整且可出库;本操作以整张销售出库单为事务边界。
        for (MesWmProductSalesLineDO other : lineService.getProductSalesLineListBySalesId(sales.getId())) {
            if (Objects.equals(other.getId(), line.getId())) continue;
            if (Boolean.TRUE.equals(other.getOqcCheckFlag())
                    && !Objects.equals(other.getQualityStatus(), MesWmQualityStatusEnum.PASS.getStatus())) {
                throw new ServiceException(400, "同一销售出库单还有未通过质检的行,不能整单出库");
            }
            var details = detailService.getProductSalesDetailListByLineId(other.getId());
            BigDecimal total = details.stream().map(MesWmProductSalesDetailDO::getQuantity)
                    .filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
            if (details.isEmpty() || other.getQuantity() == null || other.getQuantity().signum() <= 0
                    || total.compareTo(other.getQuantity()) != 0
                    || details.stream().anyMatch(d -> d.getQuantity() == null || d.getQuantity().signum() <= 0
                            || !Objects.equals(d.getSalesId(), sales.getId()) || !Objects.equals(d.getLineId(), other.getId())
                            || !Objects.equals(d.getItemId(), other.getItemId()))) {
                throw new ServiceException(400, "同一销售出库单其他行的拣货明细未备齐,不能整单出库");
            }
        }
        lineService.updateProductSalesLineWhenOqcFinish(line.getId(), qc.getId(), 1);
        MesWmProductSalesDetailSaveReqVO detail = new MesWmProductSalesDetailSaveReqVO();
        detail.setSalesId(sales.getId()); detail.setLineId(line.getId()); detail.setItemId(qc.getItemId());
        detail.setMaterialStockId(stock.getId()); detail.setQuantity(line.getQuantity());
        detail.setBatchId(stock.getBatchId()); detail.setBatchCode(stock.getBatchCode());
        detail.setWarehouseId(stock.getWarehouseId()); detail.setLocationId(stock.getLocationId()); detail.setAreaId(stock.getAreaId());
        detailService.createProductSalesDetail(detail);
        if (!salesService.checkProductSalesQuantity(sales.getId())) {
            throw new ServiceException(400, "销售出库单明细数量不完整,不能整单出库");
        }
        salesService.stockProductSales(sales.getId());
        MesWmProductSalesShippingReqVO shipping = new MesWmProductSalesShippingReqVO();
        shipping.setId(sales.getId());
        salesService.shippingProductSales(shipping);
        salesService.finishProductSales(sales.getId());
    }
 
    /** 实际出库后才能关闭让步单;不改变原始不合格检验结论。 */
    public void validateBeforeClose(MesQcNcrDO ncr) {
        MesQcOqcDO qc = oqcMapper.selectById(ncr.getQcId());
        if (qc == null || !Objects.equals(qc.getSourceDocType(), MesBizTypeConstants.WM_PRODUCT_SALES)
                || qc.getSourceDocId() == null || qc.getSourceLineId() == null || StrUtil.isBlank(qc.getBatchCode())
                || !Objects.equals(qc.getItemId(), ncr.getItemId())
                || qc.getUnqualifiedQuantity() == null || ncr.getUnqualifiedQuantity() == null
                || qc.getUnqualifiedQuantity().signum() <= 0
                || qc.getUnqualifiedQuantity().compareTo(ncr.getUnqualifiedQuantity()) != 0) {
            throw new ServiceException(400, "无法核验让步出货的来源检验单及原批次");
        }
        MesWmProductSalesDO sales = salesMapper.selectOne(new LambdaQueryWrapperX<MesWmProductSalesDO>()
                .eq(MesWmProductSalesDO::getId, qc.getSourceDocId()).last("FOR UPDATE"));
        MesWmProductSalesLineDO line = lineMapper.selectOne(new LambdaQueryWrapperX<MesWmProductSalesLineDO>()
                .eq(MesWmProductSalesLineDO::getId, qc.getSourceLineId()).last("FOR UPDATE"));
        if (sales == null || line == null || !Objects.equals(sales.getStatus(), MesWmProductSalesStatusEnum.FINISHED.getStatus())
                || !Objects.equals(line.getSalesId(), sales.getId()) || !Objects.equals(line.getOqcId(), qc.getId())
                || !Objects.equals(line.getItemId(), qc.getItemId()) || line.getQuantity() == null
                || qc.getCheckQuantity() == null || line.getQuantity().compareTo(qc.getCheckQuantity()) != 0) {
            throw new ServiceException(400, "原销售出库单尚未完成,或出库行与检验来源不一致,不能关闭");
        }
        List<MesWmTransactionDO> flows = transactionMapper.selectList(new LambdaQueryWrapperX<MesWmTransactionDO>()
                .eq(MesWmTransactionDO::getBizType, MesBizTypeConstants.WM_PRODUCT_SALES)
                .eq(MesWmTransactionDO::getBizId, sales.getId()).eq(MesWmTransactionDO::getBizLineId, line.getId())
                .eq(MesWmTransactionDO::getType, MesWmTransactionTypeEnum.OUT.getType()));
        BigDecimal out = BigDecimal.ZERO;
        for (MesWmTransactionDO flow : flows) {
            if (!Objects.equals(flow.getItemId(), qc.getItemId()) || !Objects.equals(flow.getBatchCode(), qc.getBatchCode())
                    || flow.getQuantity() == null || flow.getQuantity().signum() >= 0) {
                throw new ServiceException(400, "出库流水的物料、批次或数量与让步检验不一致");
            }
            out = out.subtract(flow.getQuantity());
        }
        if (out.compareTo(line.getQuantity()) != 0) {
            throw new ServiceException(400, "原销售出库流水数量不完整,不能关闭让步单");
        }
    }
}