gongchunyi
2026-05-15 054381d20e23e30cfb3fe962e00be6a3ded3ee2b
feat: 质检数量区分合格/不合格数量
已添加1个文件
已修改7个文件
421 ■■■■ 文件已修改
doc/quality_inspect_add_qualified_columns.sql 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java 184 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/quality/QualityInspectMapper.xml 48 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/quality_inspect_add_qualified_columns.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
ALTER TABLE `quality_inspect`
    ADD COLUMN `qualified_quantity` decimal(18, 2) NULL DEFAULT NULL COMMENT '合格数量' AFTER `quantity`,
    ADD COLUMN `unqualified_quantity` decimal(18, 2) NULL DEFAULT NULL COMMENT '不合格数量' AFTER `qualified_quantity`,
    ADD COLUMN `pass_rate` decimal(18, 2) NULL DEFAULT NULL COMMENT '合格率(%)' AFTER `unqualified_quantity`;
-- åŽ†å²æ•°æ®ï¼šæŒ‰åŽŸã€Œæ£€æµ‹ç»“æžœã€ä¸Žæ€»æ•°é‡å›žå¡«ï¼ˆæ— æ€»æ•°é‡åˆ™è·³è¿‡ï¼‰
UPDATE `quality_inspect`
SET `unqualified_quantity` = CASE WHEN `check_result` = '不合格' THEN IFNULL(`quantity`, 0) ELSE 0 END,
    `qualified_quantity`   = CASE WHEN `check_result` = '不合格' THEN 0 ELSE IFNULL(`quantity`, 0) END
WHERE `qualified_quantity` IS NULL
  AND `unqualified_quantity` IS NULL
  AND `quantity` IS NOT NULL;
UPDATE `quality_inspect`
SET `pass_rate` = CASE
                      WHEN `quantity` IS NULL OR `quantity` = 0 THEN 0
                      ELSE ROUND(IFNULL(`qualified_quantity`, 0) * 100 / `quantity`, 2)
    END
WHERE `pass_rate` IS NULL;
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java
@@ -308,6 +308,47 @@
        return analysisCustomerContractAmountsDto;
    }
    private static BigDecimal nz(BigDecimal v) {
        return v == null ? BigDecimal.ZERO : v;
    }
    /**
     * å¤§å±/报表统计用合格数量:以 qualified_quantity
     */
    private BigDecimal inspectQualifiedQtyForStat(QualityInspect item) {
        if (item.getQualifiedQuantity() != null) {
            return item.getQualifiedQuantity();
        }
        BigDecimal qty = nz(item.getQuantity());
        if ("合格".equals(item.getCheckResult())) {
            return qty;
        }
        return BigDecimal.ZERO;
    }
    /**
     * å¤§å±/报表统计用不合格数量:以 unqualified_quantity
     */
    private BigDecimal inspectUnqualifiedQtyForStat(QualityInspect item) {
        if (item.getUnqualifiedQuantity() != null) {
            return item.getUnqualifiedQuantity();
        }
        BigDecimal qty = nz(item.getQuantity());
        String cr = item.getCheckResult();
        if ("不合格".equals(cr) || "部分合格".equals(cr)) {
            return qty;
        }
        return BigDecimal.ZERO;
    }
    private boolean inspectHasUnqualifiedVolume(QualityInspect item) {
        return inspectUnqualifiedQtyForStat(item).compareTo(BigDecimal.ZERO) > 0;
    }
    private static int toStatInt(BigDecimal v) {
        return nz(v).setScale(0, RoundingMode.HALF_UP).intValue();
    }
    @Override
    public QualityStatisticsDto qualityStatistics() {
        // èŽ·å–è¿‘å››ä¸ªæœˆæ•°æ®ï¼ˆå¾€å‰æŽ¨ä¸‰ä¸ªæœˆï¼Œå…±4个完整月份)
@@ -355,29 +396,27 @@
            QualityStatisticsItem item = new QualityStatisticsItem();
            item.setDate(monthStart.format(monthFormatter)); // æ—¥æœŸæ˜¾ç¤ºä¸ºâ€œå¹´æœˆâ€ï¼ˆå¦‚ 2025-10)
            // 1. ä¾›åº”商检验(类型0)- åˆæ ¼æ•°é‡
            BigDecimal supplierQualified = monthInspects.stream()
            // 1~3:本月「不合格数量」(仅已提交检验),按检验类别拆分——用于图表图例「××不合格数」
            BigDecimal supplierUnq = monthInspects.stream()
                    .filter(inspect -> inspect.getInspectType().equals(0)
                            && "合格".equals(inspect.getCheckResult()))
                    .map(QualityInspect::getQuantity)
                            && Objects.equals(inspect.getInspectState(), 1))
                    .map(this::inspectUnqualifiedQtyForStat)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            item.setSupplierNum(supplierQualified);
            item.setSupplierNum(supplierUnq);
            // 2. å·¥åºæ£€éªŒï¼ˆç±»åž‹1)- åˆæ ¼æ•°é‡
            BigDecimal processQualified = monthInspects.stream()
            BigDecimal processUnq = monthInspects.stream()
                    .filter(inspect -> inspect.getInspectType().equals(1)
                            && "合格".equals(inspect.getCheckResult()))
                    .map(QualityInspect::getQuantity)
                            && Objects.equals(inspect.getInspectState(), 1))
                    .map(this::inspectUnqualifiedQtyForStat)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            item.setProcessNum(processQualified);
            item.setProcessNum(processUnq);
            // 3. å·¥åŽ‚æ£€éªŒï¼ˆç±»åž‹2)- åˆæ ¼æ•°é‡
            BigDecimal factoryQualified = monthInspects.stream()
            BigDecimal factoryUnq = monthInspects.stream()
                    .filter(inspect -> inspect.getInspectType().equals(2)
                            && "合格".equals(inspect.getCheckResult()))
                    .map(QualityInspect::getQuantity)
                            && Objects.equals(inspect.getInspectState(), 1))
                    .map(this::inspectUnqualifiedQtyForStat)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            item.setFactoryNum(factoryQualified);
            item.setFactoryNum(factoryUnq);
            qualityStatisticsItems.add(item);
        }
@@ -1902,17 +1941,14 @@
        BigDecimal unqualifiedCount = BigDecimal.ZERO;
        for (QualityInspect item : list) {
            if ("合格".equals(item.getCheckResult())) {
                qualifiedCount = qualifiedCount.add(item.getQuantity());
            } else {
                unqualifiedCount = unqualifiedCount.add(item.getQuantity());
            }
            qualifiedCount = qualifiedCount.add(inspectQualifiedQtyForStat(item));
            unqualifiedCount = unqualifiedCount.add(inspectUnqualifiedQtyForStat(item));
        }
        BigDecimal totalCount = qualifiedCount.add(unqualifiedCount);
        dto.setQualifiedCount(qualifiedCount.intValue());
        dto.setUnqualifiedCount(unqualifiedCount.intValue());
        dto.setQualifiedCount(toStatInt(qualifiedCount));
        dto.setUnqualifiedCount(toStatInt(unqualifiedCount));
        if (totalCount.compareTo(BigDecimal.ZERO) == 0) {
            dto.setQualifiedRate(BigDecimal.ZERO.setScale(2));
@@ -2175,13 +2211,8 @@
                continue;
            }
            BigDecimal quantity = item.getQuantity();
            if ("合格".equals(item.getCheckResult())) {
                dto.setQualifiedCount(dto.getQualifiedCount().add(quantity));
            } else {
                dto.setUnqualifiedCount(dto.getUnqualifiedCount().add(quantity));
            }
            dto.setQualifiedCount(dto.getQualifiedCount().add(inspectQualifiedQtyForStat(item)));
            dto.setUnqualifiedCount(dto.getUnqualifiedCount().add(inspectUnqualifiedQtyForStat(item)));
        }
        // è®¡ç®—合格率
@@ -2222,14 +2253,11 @@
            BigDecimal unqualifiedCount = BigDecimal.ZERO;
            for (QualityInspect item : items) {
                BigDecimal qty = item.getQuantity();
                BigDecimal qty = nz(item.getQuantity());
                totalCount = totalCount.add(qty);
                if ("合格".equals(item.getCheckResult())) {
                    qualifiedCount = qualifiedCount.add(qty);
                } else {
                    unqualifiedCount = unqualifiedCount.add(qty);
                }
                qualifiedCount = qualifiedCount.add(inspectQualifiedQtyForStat(item));
                unqualifiedCount = unqualifiedCount.add(inspectUnqualifiedQtyForStat(item));
            }
            if (totalCount.compareTo(BigDecimal.ZERO) == 0) {
@@ -2358,10 +2386,10 @@
        dto.setProcessNum(sumQuantity(qualityInspectList, 1)); // è¿‡ç¨‹
        dto.setFactoryNum(sumQuantity(qualityInspectList, 2)); // å‡ºåŽ‚
        // å‡è®¾ qualityInspectList æ˜¯ä¸€ä¸ª List<QualityInspect> ç±»åž‹çš„集合
        Map<String, List<QualityInspect>> groupedByCheckResult = qualityInspectList.stream()
                .collect(Collectors.groupingBy(QualityInspect::getCheckResult));
        List<QualityInspect> qualityInspects = groupedByCheckResult.get("不合格");
        // å­˜åœ¨ä¸åˆæ ¼æ•°é‡çš„æ£€éªŒå•(含按数量拆分的新数据;旧数据仍可用检测结果兜底)
        List<QualityInspect> qualityInspects = qualityInspectList.stream()
                .filter(this::inspectHasUnqualifiedVolume)
                .collect(Collectors.toList());
        if (ObjectUtils.isNull(qualityInspects) || qualityInspects.size() == 0) {
            return dto;
        }
@@ -2416,11 +2444,11 @@
        QualityStatisticsItem item = new QualityStatisticsItem();
        item.setDate(dateLabel);
        item.setSupplierNum(list.stream().filter(i -> i.getInspectType() == 0).map(QualityInspect::getQuantity)
        item.setSupplierNum(list.stream().filter(i -> i.getInspectType() == 0).map(this::inspectUnqualifiedQtyForStat)
                .reduce(BigDecimal.ZERO, BigDecimal::add));
        item.setProcessNum(list.stream().filter(i -> i.getInspectType() == 1).map(QualityInspect::getQuantity)
        item.setProcessNum(list.stream().filter(i -> i.getInspectType() == 1).map(this::inspectUnqualifiedQtyForStat)
                .reduce(BigDecimal.ZERO, BigDecimal::add));
        item.setFactoryNum(list.stream().filter(i -> i.getInspectType() == 2).map(QualityInspect::getQuantity)
        item.setFactoryNum(list.stream().filter(i -> i.getInspectType() == 2).map(this::inspectUnqualifiedQtyForStat)
                .reduce(BigDecimal.ZERO, BigDecimal::add));
        return item;
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java
@@ -33,6 +33,7 @@
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@@ -187,6 +188,10 @@
                qualityInspect.setModel(productModel.getModel());
                qualityInspect.setUnit(productModel.getUnit());
                qualityInspect.setQuantity(productQty);
                qualityInspect.setQualifiedQuantity(productQty);
                qualityInspect.setUnqualifiedQuantity(BigDecimal.ZERO);
                qualityInspect.setPassRate(BigDecimal.valueOf(100).setScale(2, RoundingMode.HALF_UP));
                qualityInspect.setCheckResult("合格");
                qualityInspect.setProcess(process);
                qualityInspect.setInspectState(0);
                qualityInspect.setInspectType(inspectType);
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java
@@ -272,6 +272,9 @@
        qualityInspect.setProductModelId(saleProduct.getProductModelId());
        qualityInspect.setUnit(saleProduct.getUnit());
        qualityInspect.setQuantity(saleProduct.getQuantity());
        qualityInspect.setQualifiedQuantity(saleProduct.getQuantity());
        qualityInspect.setUnqualifiedQuantity(BigDecimal.ZERO);
        qualityInspect.setPassRate(BigDecimal.valueOf(100).setScale(2, RoundingMode.HALF_UP));
        qualityInspectMapper.insert(qualityInspect);
        List<QualityTestStandard> qualityTestStandard = qualityTestStandardMapper.getQualityTestStandardByProductId(saleProduct.getProductId(), 0, null);
        if (qualityTestStandard.size() > 0) {
@@ -931,7 +934,7 @@
            }
            //  éœ€è¦è´¨æ£€ï¼šæ‰«ç å…¥åº“进入原材料检验,不直接入合格库存
            if (Boolean.TRUE.equals(dbProduct.getIsChecked())) {
                //  å­˜åœ¨æœªé€šè¿‡/未处理的原材料检验单,则禁止继续扫码入库
                //  å­˜åœ¨æœªæäº¤æˆ–入库审批中的原材料检验单,则禁止继续扫码入库
                Long pendingInspectCount = qualityInspectMapper.selectCount(new LambdaQueryWrapper<QualityInspect>()
                        .eq(QualityInspect::getInspectType, 0)
                        .eq(QualityInspect::getPurchaseLedgerId, purchaseId)
@@ -939,9 +942,8 @@
                        .and(w -> w
                                .isNull(QualityInspect::getInspectState)
                                .or(q0 -> q0.eq(QualityInspect::getInspectState, 0))
                                // inspect_state=1 ä¹Ÿè§†ä¸ºâ€œæœªå¤„理”
                                .or(q1 -> q1.eq(QualityInspect::getInspectState, 1)
                                        .isNull(QualityInspect::getCheckResult))));
                                        .in(QualityInspect::getApprovalStatus, 1, 2))));
                if (pendingInspectCount != null && pendingInspectCount > 0) {
                    throw new ServiceException("入库失败,存在未通过或未处理的质检记录,请先处理后再扫码入库");
                }
@@ -956,9 +958,11 @@
                                        .or(q1 -> q1.eq(QualityInspect::getInspectState, 1)
                                                .and(r -> r.isNull(QualityInspect::getCheckResult)
                                                        .or()
                                                        .eq(QualityInspect::getCheckResult, "合格")))))
                                                        .eq(QualityInspect::getCheckResult, "合格")
                                                        .or()
                                                        .eq(QualityInspect::getCheckResult, "部分合格")))))
                        .stream()
                        .map(QualityInspect::getQuantity)
                        .map(this::resolveInspectCountedQuantity)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add);
                if (inspectQty.add(inboundThisLine).compareTo(orderQty) > 0) {
@@ -1332,4 +1336,20 @@
        }
        return sb.toString();
    }
    private BigDecimal resolveInspectCountedQuantity(QualityInspect inspect) {
        if (inspect == null) {
            return BigDecimal.ZERO;
        }
        if (inspect.getInspectState() == null || inspect.getInspectState() == 0) {
            return inspect.getQuantity() == null ? BigDecimal.ZERO : inspect.getQuantity();
        }
        if (inspect.getQualifiedQuantity() != null) {
            return inspect.getQualifiedQuantity();
        }
        if ("合格".equals(inspect.getCheckResult()) || "部分合格".equals(inspect.getCheckResult())) {
            return inspect.getQuantity() == null ? BigDecimal.ZERO : inspect.getQuantity();
        }
        return BigDecimal.ZERO;
    }
}
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java
@@ -94,12 +94,30 @@
    private String unit;
    /**
     * æ•°é‡
     * æ•°é‡ï¼ˆæ€»æ•°é‡ï¼Œåˆ›å»ºåŽä¸å¯ä¿®æ”¹ï¼‰
     */
    @Excel(name = "数量")
    private BigDecimal quantity;
    /**
     * åˆæ ¼æ•°é‡
     */
    @Excel(name = "合格数量")
    private BigDecimal qualifiedQuantity;
    /**
     * ä¸åˆæ ¼æ•°é‡
     */
    @Excel(name = "不合格数量")
    private BigDecimal unqualifiedQuantity;
    /**
     * åˆæ ¼çŽ‡ï¼ˆ%)
     */
    @Excel(name = "合格率")
    private BigDecimal passRate;
    /**
     * æ£€æµ‹å•位
     */
    @Excel(name = "检测单位")
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
@@ -42,6 +42,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
@@ -74,6 +75,9 @@
        QualityInspect qualityInspect = new QualityInspect();
        BeanUtils.copyProperties(qualityInspectDto, qualityInspect);
        qualityInspect.setInspectState(0);//默认未提交
        ensureQualifiedSplitDefaults(qualityInspect);
        // å‰ç«¯è‹¥å·²ä¼ åˆæ ¼/不合格数量,defaults ä¸ä¼šå†™ pass_rate,这里补算列表展示用合格率
        refreshPassRateFromQuantities(qualityInspect);
        qualityInspectMapper.insert(qualityInspect);
        for (QualityInspectParam qualityInspectParam : qualityInspectDto.getQualityInspectParams()) {
            qualityInspectParam.setInspectId(qualityInspect.getId());
@@ -96,41 +100,147 @@
    @Override
    public int submit(QualityInspect inspect) {
        QualityInspect qualityInspect = qualityInspectMapper.selectById(inspect.getId());
        //提交前必须判断是否合格
        if (ObjectUtils.isNull(qualityInspect.getCheckResult())) {
            throw new RuntimeException("请先判断是否合格");
        if (qualityInspect == null) {
            throw new RuntimeException("质检单不存在");
        }
        /*判断不合格*/
        if (qualityInspect.getCheckResult().equals("不合格")) {
        if (Objects.equals(qualityInspect.getInspectState(), 1)) {
            throw new RuntimeException("该质检单已提交,不能重复提交");
        }
        if (inspect != null) {
            if (inspect.getQualifiedQuantity() != null) {
                qualityInspect.setQualifiedQuantity(inspect.getQualifiedQuantity());
            }
            if (inspect.getUnqualifiedQuantity() != null) {
                qualityInspect.setUnqualifiedQuantity(inspect.getUnqualifiedQuantity());
            }
        }
        validateAndCalculateQuantities(qualityInspect);
        BigDecimal qualifiedQty = qualityInspect.getQualifiedQuantity();
        BigDecimal unqualifiedQty = qualityInspect.getUnqualifiedQuantity();
        if (unqualifiedQty.compareTo(BigDecimal.ZERO) > 0) {
            QualityUnqualified qualityUnqualified = new QualityUnqualified();
            BeanUtils.copyProperties(qualityInspect, qualityUnqualified);
            qualityUnqualified.setInspectState(0);//待处理
            List<QualityInspectParam> inspectParams = qualityInspectParamService.list(Wrappers.<QualityInspectParam>lambdaQuery().eq(QualityInspectParam::getInspectId, inspect.getId()));
            String text = inspectParams.stream().map(QualityInspectParam::getParameterItem).collect(Collectors.joining(","));
            qualityUnqualified.setDefectivePhenomena(text + "这些指标中存在不合格");//不合格现象
            qualityUnqualified.setId(null);
            qualityUnqualified.setQuantity(unqualifiedQty);
            qualityUnqualified.setInspectState(0);
            qualityUnqualified.setDefectivePhenomena(buildDefectivePhenomena(qualityInspect));
            qualityUnqualified.setInspectId(qualityInspect.getId());
            qualityUnqualifiedMapper.insert(qualityUnqualified);
        } else {
            // åŽŸææ–™æ£€éªŒåˆæ ¼åŽï¼Œå…ˆå‘èµ·â€œå…¥åº“å®¡æ‰¹â€ï¼Œå®¡æ‰¹é€šè¿‡åŽå†å…¥åº“
        }
        if (qualifiedQty.compareTo(BigDecimal.ZERO) > 0) {
            if (Objects.equals(qualityInspect.getInspectType(), 0)) {
                submitQualifiedInboundApprove(qualityInspect);
                Long ledgerId = qualityInspect.getPurchaseLedgerId();
                PurchaseLedger purchaseLedger = ledgerId == null ? null : purchaseLedgerMapper.selectById(ledgerId);
                if (purchaseLedger != null) {
                    submitQualifiedInboundApprove(qualityInspect);
                } else {
                    // æ‰‹åŠ¨æ–°å¢žçš„åŽŸææ–™æ£€éªŒï¼šæ— é‡‡è´­å°è´¦ï¼Œä¸èµ°é‡‡è´­å…¥åº“å®¡æ‰¹ï¼Œç›´æŽ¥å…¥åˆæ ¼åº“å­˜ï¼ˆä¸Žè¿‡ç¨‹/出厂检验一致)
                    stockUtils.addStock(
                            null,
                            null,
                            qualityInspect.getProductModelId(),
                            qualifiedQty,
                            StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode(),
                            qualityInspect.getId()
                    );
                    syncQualifiedInboundToPurchaseProducts(qualityInspect, qualifiedQty);
                }
            } else {
                //  ç›´æŽ¥å…¥åº“
                stockUtils.addStock(
                        qualityInspect.getPurchaseLedgerId() == null ? null : qualityInspect.getPurchaseLedgerId().longValue(),
                        qualityInspect.getPurchaseLedgerId() == null ? null : qualityInspect.getPurchaseLedgerId(),
                        null,
                        qualityInspect.getProductModelId(),
                        qualityInspect.getQuantity(),
                        qualifiedQty,
                        StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode(),
                        qualityInspect.getId()
                );
                syncQualifiedInboundToPurchaseProducts(qualityInspect);
                syncQualifiedInboundToPurchaseProducts(qualityInspect, qualifiedQty);
            }
        }
        qualityInspect.setInspectState(1);//已提交
        qualityInspect.setCheckResult(resolveCheckResult(qualifiedQty, unqualifiedQty));
        qualityInspect.setInspectState(1);
        int updated = qualityInspectMapper.updateById(qualityInspect);
        refreshPurchaseLedgerStockStatusByInspect(qualityInspect.getPurchaseLedgerId());
        return updated;
    }
    private void validateAndCalculateQuantities(QualityInspect qualityInspect) {
        if (qualityInspect.getQualifiedQuantity() == null || qualityInspect.getUnqualifiedQuantity() == null) {
            throw new RuntimeException("请填写合格数量和不合格数量");
        }
        if (qualityInspect.getQualifiedQuantity().compareTo(BigDecimal.ZERO) < 0
                || qualityInspect.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) < 0) {
            throw new RuntimeException("合格数量和不合格数量不能为负数");
        }
        if (qualityInspect.getQuantity() == null) {
            throw new RuntimeException("质检单总数量异常");
        }
        BigDecimal total = qualityInspect.getQualifiedQuantity().add(qualityInspect.getUnqualifiedQuantity());
        BigDecimal qtyScaled = qualityInspect.getQuantity().setScale(4, RoundingMode.HALF_UP);
        BigDecimal sumScaled = total.setScale(4, RoundingMode.HALF_UP);
        if (sumScaled.compareTo(qtyScaled) > 0) {
            throw new RuntimeException("合格数量与不合格数量之和不能超过总数量");
        }
        qualityInspect.setPassRate(calculatePassRate(qualityInspect.getQualifiedQuantity(), qualityInspect.getQuantity()));
    }
    /**
     * æ–°å¢žæ—¶è‹¥æœªæ‹†åˆ†åˆæ ¼/不合格,默认全部为待检合格数
     */
    private void ensureQualifiedSplitDefaults(QualityInspect q) {
        if (q.getQuantity() == null) {
            return;
        }
        if (q.getQualifiedQuantity() == null && q.getUnqualifiedQuantity() == null) {
            q.setQualifiedQuantity(q.getQuantity());
            q.setUnqualifiedQuantity(BigDecimal.ZERO);
            q.setPassRate(calculatePassRate(q.getQualifiedQuantity(), q.getQuantity()));
            if (q.getCheckResult() == null || q.getCheckResult().isEmpty()) {
                q.setCheckResult("合格");
            }
        }
    }
    private BigDecimal calculatePassRate(BigDecimal qualifiedQty, BigDecimal totalQty) {
        if (totalQty == null || totalQty.compareTo(BigDecimal.ZERO) <= 0 || qualifiedQty == null) {
            return BigDecimal.ZERO;
        }
        return qualifiedQty.multiply(BigDecimal.valueOf(100))
                .divide(totalQty, 2, RoundingMode.HALF_UP);
    }
    private void refreshPassRateFromQuantities(QualityInspect q) {
        if (q.getQuantity() == null || q.getQualifiedQuantity() == null) {
            return;
        }
        q.setPassRate(calculatePassRate(q.getQualifiedQuantity(), q.getQuantity()));
    }
    private String resolveCheckResult(BigDecimal qualifiedQty, BigDecimal unqualifiedQty) {
        if (unqualifiedQty.compareTo(BigDecimal.ZERO) <= 0) {
            return "合格";
        }
        if (qualifiedQty.compareTo(BigDecimal.ZERO) <= 0) {
            return "不合格";
        }
        return "部分合格";
    }
    private String buildDefectivePhenomena(QualityInspect qualityInspect) {
        if (ObjectUtils.isNotEmpty(qualityInspect.getDefectivePhenomena())) {
            return qualityInspect.getDefectivePhenomena();
        }
        List<QualityInspectParam> inspectParams = qualityInspectParamService.list(
                Wrappers.<QualityInspectParam>lambdaQuery().eq(QualityInspectParam::getInspectId, qualityInspect.getId()));
        if (inspectParams.isEmpty()) {
            return "质检不合格数量:" + qualityInspect.getUnqualifiedQuantity();
        }
        String text = inspectParams.stream().map(QualityInspectParam::getParameterItem).collect(Collectors.joining(","));
        return text + "等指标检验不合格,不合格数量:" + qualityInspect.getUnqualifiedQuantity();
    }
    private void submitQualifiedInboundApprove(QualityInspect qualityInspect) {
@@ -188,18 +298,22 @@
        if (!Objects.equals(qualityInspect.getInspectType(), 0)) {
            throw new RuntimeException("审批失败,仅原材料检验支持入库审批");
        }
        if (!Objects.equals(qualityInspect.getInspectState(), 1) || !"合格".equals(qualityInspect.getCheckResult())) {
        if (!Objects.equals(qualityInspect.getInspectState(), 1)) {
            throw new RuntimeException("审批失败,当前质检单状态不允许入库");
        }
        BigDecimal qualifiedQty = qualityInspect.getQualifiedQuantity();
        if (qualifiedQty == null || qualifiedQty.compareTo(BigDecimal.ZERO) <= 0) {
            throw new RuntimeException("审批失败,无合格数量可入库");
        }
        stockUtils.addStock(
                qualityInspect.getPurchaseLedgerId() == null ? null : qualityInspect.getPurchaseLedgerId().longValue(),
                null,
                qualityInspect.getProductModelId(),
                qualityInspect.getQuantity(),
                qualifiedQty,
                StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode(),
                qualityInspect.getId()
        );
        syncQualifiedInboundToPurchaseProducts(qualityInspect);
        syncQualifiedInboundToPurchaseProducts(qualityInspect, qualifiedQty);
        refreshPurchaseLedgerStockStatusByInspect(qualityInspect.getPurchaseLedgerId());
        qualityInspect.setApprovalStatus(3);
        qualityInspectMapper.updateById(qualityInspect);
@@ -272,6 +386,13 @@
    @Override
    public int updateQualityInspect(QualityInspectDto qualityInspectDto) {
        QualityInspect existing = qualityInspectMapper.selectById(qualityInspectDto.getId());
        if (existing == null) {
            throw new RuntimeException("质检单不存在");
        }
        if (Objects.equals(existing.getInspectState(), 1)) {
            throw new RuntimeException("已提交的数据不允许修改");
        }
        if (ObjectUtils.isNotNull(qualityInspectDto.getQualityInspectParams())) {
            qualityInspectParamService.remove(Wrappers.<QualityInspectParam>lambdaQuery().eq(QualityInspectParam::getInspectId, qualityInspectDto.getId()));
            for (QualityInspectParam qualityInspectParam : qualityInspectDto.getQualityInspectParams()) {
@@ -281,6 +402,24 @@
        }
        QualityInspect qualityInspect = new QualityInspect();
        BeanUtils.copyProperties(qualityInspectDto, qualityInspect);
        qualityInspect.setQuantity(existing.getQuantity());
        BigDecimal qf = qualityInspect.getQualifiedQuantity() != null ? qualityInspect.getQualifiedQuantity() : existing.getQualifiedQuantity();
        BigDecimal uqf = qualityInspect.getUnqualifiedQuantity() != null ? qualityInspect.getUnqualifiedQuantity() : existing.getUnqualifiedQuantity();
        if (qf == null || uqf == null) {
            BigDecimal qty = existing.getQuantity() != null ? existing.getQuantity() : BigDecimal.ZERO;
            if ("不合格".equals(existing.getCheckResult())) {
                qf = BigDecimal.ZERO;
                uqf = qty;
            } else {
                qf = qty;
                uqf = BigDecimal.ZERO;
            }
        }
        qualityInspect.setQualifiedQuantity(qf);
        qualityInspect.setUnqualifiedQuantity(uqf);
        validateAndCalculateQuantities(qualityInspect);
        qualityInspect.setCheckResult(resolveCheckResult(qf, uqf));
        return qualityInspectMapper.updateById(qualityInspect);
    }
@@ -340,17 +479,16 @@
        }
    }
    private void syncQualifiedInboundToPurchaseProducts(QualityInspect qualityInspect) {
    private void syncQualifiedInboundToPurchaseProducts(QualityInspect qualityInspect, BigDecimal inboundQty) {
        if (qualityInspect == null) {
            return;
        }
        if (!Objects.equals(qualityInspect.getInspectType(), 0) || qualityInspect.getPurchaseLedgerId() == null) {
            return;
        }
        if (qualityInspect.getProductModelId() == null || qualityInspect.getQuantity() == null) {
        if (qualityInspect.getProductModelId() == null || inboundQty == null) {
            return;
        }
        BigDecimal inboundQty = qualityInspect.getQuantity();
        if (inboundQty.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
@@ -2965,6 +2965,9 @@
        qualityInspect.setModel(dbProduct.getSpecificationModel());
        qualityInspect.setUnit(resolveInspectUnit(dbProduct));
        qualityInspect.setQuantity(inspectQty);
        qualityInspect.setQualifiedQuantity(inspectQty);
        qualityInspect.setUnqualifiedQuantity(BigDecimal.ZERO);
        qualityInspect.setPassRate(BigDecimal.valueOf(100));
        qualityInspect.setCheckResult("合格");
        qualityInspect.setInspectState(1);
        qualityInspect.setApprovalStatus(1);
src/main/resources/mapper/quality/QualityInspectMapper.xml
@@ -15,6 +15,9 @@
        qi.model,
        ifnull(pm.unit, qi.unit) as unit,
        qi.quantity,
        qi.qualified_quantity,
        qi.unqualified_quantity,
        qi.pass_rate,
        qi.check_company,
        qi.check_result,
        qi.create_time,
@@ -177,10 +180,14 @@
        SELECT base.modelType,
               COALESCE(SUM(qi.quantity), 0)                                                AS totalCount,
               COALESCE(SUM(CASE WHEN qi.inspect_state = 1 THEN qi.quantity ELSE 0 END), 0) AS completedCount,
               COALESCE(SUM(CASE WHEN qi.inspect_state = 1 AND qi.check_result = '合格' THEN qi.quantity ELSE 0 END),
                        0)                                                                  AS qualifiedCount,
               COALESCE(SUM(CASE WHEN qi.inspect_state = 1 AND qi.check_result = '不合格' THEN qi.quantity ELSE 0 END),
                        0)                                                                  AS unqualifiedCount,
               COALESCE(SUM(CASE
                                  WHEN qi.inspect_state = 1 THEN IFNULL(qi.qualified_quantity,
                                                                          CASE WHEN qi.check_result = '合格' THEN IFNULL(qi.quantity, 0) ELSE 0 END)
                                  ELSE 0 END), 0)                                  AS qualifiedCount,
               COALESCE(SUM(CASE
                                  WHEN qi.inspect_state = 1 THEN IFNULL(qi.unqualified_quantity,
                                                                        CASE WHEN qi.check_result IN ('不合格', '部分合格') THEN IFNULL(qi.quantity, 0) ELSE 0 END)
                                  ELSE 0 END), 0)                                  AS unqualifiedCount,
               IF(COALESCE(SUM(qi.quantity), 0) = 0, 0,
                  ROUND(SUM(CASE WHEN qi.inspect_state = 1 THEN qi.quantity ELSE 0 END)
@@ -188,7 +195,10 @@
                   )                                                                        AS completionRate,
               IF(SUM(CASE WHEN qi.inspect_state = 1 THEN qi.quantity ELSE 0 END) = 0, 0,
                  ROUND(SUM(CASE WHEN qi.inspect_state = 1 AND qi.check_result = '合格' THEN qi.quantity ELSE 0 END)
                  ROUND(SUM(CASE
                                WHEN qi.inspect_state = 1 THEN IFNULL(qi.qualified_quantity,
                                                                        CASE WHEN qi.check_result = '合格' THEN IFNULL(qi.quantity, 0) ELSE 0 END)
                                ELSE 0 END)
                            / SUM(CASE WHEN qi.inspect_state = 1 THEN qi.quantity ELSE 0 END) * 100, 2)
                   )                                                                        AS passRate
@@ -249,10 +259,14 @@
               COALESCE(SUM(qi.quantity), 0)                                                AS totalCount,
               COALESCE(SUM(CASE WHEN qi.inspect_state = 1 THEN qi.quantity ELSE 0 END), 0) AS completedCount,
               COALESCE(SUM(CASE WHEN qi.inspect_state = 1 AND qi.check_result = '合格' THEN qi.quantity ELSE 0 END),
                        0)                                                                  AS qualifiedCount,
               COALESCE(SUM(CASE WHEN qi.inspect_state = 1 AND qi.check_result = '不合格' THEN qi.quantity ELSE 0 END),
                        0)                                                                  AS unqualifiedCount,
               COALESCE(SUM(CASE
                                  WHEN qi.inspect_state = 1 THEN IFNULL(qi.qualified_quantity,
                                                                          CASE WHEN qi.check_result = '合格' THEN IFNULL(qi.quantity, 0) ELSE 0 END)
                                  ELSE 0 END), 0)                                  AS qualifiedCount,
               COALESCE(SUM(CASE
                                  WHEN qi.inspect_state = 1 THEN IFNULL(qi.unqualified_quantity,
                                                                        CASE WHEN qi.check_result IN ('不合格', '部分合格') THEN IFNULL(qi.quantity, 0) ELSE 0 END)
                                  ELSE 0 END), 0)                                  AS unqualifiedCount,
            /* å®Œæˆçއ */
               IF(COALESCE(SUM(qi.quantity), 0) = 0, 0,
@@ -261,7 +275,10 @@
            /* åˆæ ¼çއ */
               IF(SUM(CASE WHEN qi.inspect_state = 1 THEN qi.quantity ELSE 0 END) = 0, 0,
                  ROUND(SUM(CASE WHEN qi.inspect_state = 1 AND qi.check_result = '合格' THEN qi.quantity ELSE 0 END)
                  ROUND(SUM(CASE
                                WHEN qi.inspect_state = 1 THEN IFNULL(qi.qualified_quantity,
                                                                        CASE WHEN qi.check_result = '合格' THEN IFNULL(qi.quantity, 0) ELSE 0 END)
                                ELSE 0 END)
                            / SUM(CASE WHEN qi.inspect_state = 1 THEN qi.quantity ELSE 0 END) * 100, 2)
                   )                                                                        AS passRate
@@ -306,11 +323,16 @@
               COALESCE(SUM(CASE WHEN qi.inspect_state = 1 THEN qi.quantity ELSE 0 END), 0) AS completedCount,
               COALESCE(SUM(CASE WHEN qi.inspect_state = 1 AND qi.check_result = '合格' THEN qi.quantity ELSE 0 END),
                        0)                                                                  AS qualifiedCount,
               COALESCE(SUM(CASE
                                  WHEN qi.inspect_state = 1 THEN IFNULL(qi.qualified_quantity,
                                                                          CASE WHEN qi.check_result = '合格' THEN IFNULL(qi.quantity, 0) ELSE 0 END)
                                  ELSE 0 END), 0)                                  AS qualifiedCount,
               IF(SUM(CASE WHEN qi.inspect_state = 1 THEN qi.quantity ELSE 0 END) = 0, 0,
                  ROUND(SUM(CASE WHEN qi.inspect_state = 1 AND qi.check_result = '合格' THEN qi.quantity ELSE 0 END)
                  ROUND(SUM(CASE
                                WHEN qi.inspect_state = 1 THEN IFNULL(qi.qualified_quantity,
                                                                        CASE WHEN qi.check_result = '合格' THEN IFNULL(qi.quantity, 0) ELSE 0 END)
                                ELSE 0 END)
                            / SUM(CASE WHEN qi.inspect_state = 1 THEN qi.quantity ELSE 0 END) * 100, 2)
                   )                                                                        AS passRate