gaoluyang
5 天以前 6fa82a1ad7c3ae061ef499f0d0806a16846e122b
feat(mes): 来料检验与不合格退货闭环优化

- 采购台账列表返回到货通知信息(单号/状态),采购明细强制为需来料检验
- 到货通知列表返回质检进度(需检行/已建检验单行/已检验行)
- IQC/OQC 由来源单据生成时不再默认合格数量为 0,检测数量改为合格+不合格汇总
- 不合格单采购处置放宽到货通知状态校验:待质检/待入库/已完成均允许
- 不合格退货处置直接生成供应商退货单并落到采购退货列表,同步质量侧台账

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加1个文件
已修改17个文件
290 ■■■■■ 文件已修改
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderImportExcelVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/arrivalnotice/MesArrivalNoticeApi.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/arrivalnotice/dto/MesArrivalNoticeRespDTO.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/arrivalnotice/MesArrivalNoticeApiImpl.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/arrivalnotice/MesWmArrivalNoticeController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/arrivalnotice/vo/MesWmArrivalNoticeRespVO.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/arrivalnotice/MesWmArrivalNoticeLineMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/arrivalnotice/MesWmArrivalNoticeMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/iqc/MesQcIqcServiceImpl.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ncr/MesQcNcrPurchaseSourceValidator.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ncr/MesQcNcrServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ncr/MesQcPurchaseReturnService.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeLineService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeLineServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java
@@ -26,6 +26,8 @@
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mes.api.arrivalnotice.MesArrivalNoticeApi;
import cn.iocoder.yudao.module.mes.api.arrivalnotice.dto.MesArrivalNoticeRespDTO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
@@ -75,6 +77,8 @@
    private AdminUserApi adminUserApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Resource
    private MesArrivalNoticeApi arrivalNoticeApi;
    @PostMapping("/create")
    @Operation(summary = "创建采购订单")
@@ -186,7 +190,7 @@
                        .no("CGDD20260101000001").supplierName("示例供应商")
                        .orderTime("2026-01-01 10:00:00").productCode("ITEM001").productName("示例产品A")
                        .count(new BigDecimal("100")).productPrice(new BigDecimal("10.00"))
                        .taxPercent(new BigDecimal("13")).qcCheckFlag("否").remark("示例").build());
                        .taxPercent(new BigDecimal("13")).remark("示例").build());
        ExcelUtils.write(response, "采购订单导入模板.xls", "采购订单", ErpPurchaseOrderImportExcelVO.class, list);
    }
@@ -261,6 +265,11 @@
        // 1.6 å·²æ¥ç¥¨é‡‘额:按采购订单统计,用于计算剩余可开票金额
        Map<Long, BigDecimal> invoicePriceSumMap = purchaseInvoiceService
                .getPurchaseInvoicePriceSumMapByOrderIds(convertSet(pageResult.getList(), ErpPurchaseOrderDO::getId));
        // 1.7 åˆ°è´§é€šçŸ¥å•:用于前端区分是否已确认收货、隐藏重复的「确认收货」按钮
        Map<Long, MesArrivalNoticeRespDTO> arrivalNoticeMap = arrivalNoticeApi
                .getArrivalNoticeListByPurchaseOrderIds(convertSet(pageResult.getList(), ErpPurchaseOrderDO::getId))
                .stream().collect(java.util.stream.Collectors.toMap(
                        MesArrivalNoticeRespDTO::getPurchaseOrderId, notice -> notice, (a, b) -> a));
        // 2. å¼€å§‹æ‹¼æŽ¥
        return BeanUtils.toBean(pageResult, ErpPurchaseOrderRespVO.class, purchaseOrder -> {
            purchaseOrder.setItems(BeanUtils.toBean(
@@ -279,6 +288,12 @@
            purchaseOrder.setProductCodes(CollUtil.join(purchaseOrder.getItems(), ",", ErpPurchaseOrderRespVO.Item::getProductCode));
            MapUtils.findAndThen(supplierMap, purchaseOrder.getSupplierId(), supplier -> purchaseOrder.setSupplierName(supplier.getName()));
            MapUtils.findAndThen(userMap, Long.parseLong(purchaseOrder.getCreator()), user -> purchaseOrder.setCreatorName(user.getNickname()));
            MapUtils.findAndThen(arrivalNoticeMap, purchaseOrder.getId(), notice -> {
                purchaseOrder.setArrivalNoticeId(notice.getId());
                purchaseOrder.setArrivalNoticeCode(notice.getCode());
                purchaseOrder.setArrivalNoticeStatus(notice.getStatus());
                purchaseOrder.setArrivalNoticeStatusName(notice.getStatusName());
            });
            purchaseOrder.setSourceTypeName(resolveSourceTypeName(purchaseOrder.getSourceType()));
            // å‰©ä½™å¯å¼€ç¥¨é‡‘额 = è®¢å•总金额 - å·²æ¥ç¥¨é‡‘额
            purchaseOrder.setRemainInvoicePrice(purchaseOrder.getTotalPrice().subtract(
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderImportExcelVO.java
@@ -76,10 +76,6 @@
    @Schema(description = "税率,百分比,例如 13 è¡¨ç¤º 13%")
    private BigDecimal taxPercent;
    @ExcelProperty("是否来料检验")
    @Schema(description = "是否需要来料检验:填“是”为需要,其它为不需要")
    private String qcCheckFlag;
    @ExcelProperty("备注")
    @Schema(description = "备注")
    private String remark;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java
@@ -147,6 +147,20 @@
    @Schema(description = "入库状态(0:未入库 1:部分入库 2:全部入库)", example = "0")
    private Integer inStatus;
    // ========== ç¡®è®¤æ”¶è´§ï¼ˆåˆ°è´§é€šçŸ¥ï¼‰ ==========
    @Schema(description = "到货通知单编号,已确认收货时返回", example = "15")
    private Long arrivalNoticeId;
    @Schema(description = "到货通知单号", example = "AN1794282646472")
    private String arrivalNoticeCode;
    @Schema(description = "到货通知单状态(0:草稿 2:待质检 3:待入库 4:已完成)", example = "3")
    private Integer arrivalNoticeStatus;
    @Schema(description = "到货通知单状态名称", example = "待入库")
    private String arrivalNoticeStatusName;
    // ========== é‡‡è´­é€€è´§ï¼ˆå‡ºåº“)) ==========
    @Schema(description = "采购退货数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
@@ -300,7 +300,8 @@
            itemVO.setCount(row.getCount());
            itemVO.setTaxPercent(row.getTaxPercent());
            itemVO.setRemark(row.getRemark());
            itemVO.setQcCheckFlag("是".equals(StrUtil.trim(row.getQcCheckFlag())));
            // é‡‡è´­å°è´¦ä¸å†æä¾›ã€Œæ˜¯å¦è´¨æ£€ã€é€‰æ‹©ï¼šå¯¼å…¥çš„æ˜Žç»†ä¸€å¾‹éœ€è¦æ¥æ–™æ£€éªŒ
            itemVO.setQcCheckFlag(true);
            items.add(itemVO);
        }
@@ -611,6 +612,8 @@
    private List<ErpPurchaseOrderItemDO> validatePurchaseOrderItems(List<ErpPurchaseOrderSaveReqVO.Item> list) {
        // è½¬åŒ–为 ErpPurchaseOrderItemDO åˆ—表(产品验证已移除)
        List<ErpPurchaseOrderItemDO> items = convertList(list, o -> BeanUtils.toBean(o, ErpPurchaseOrderItemDO.class, item -> {
            // é‡‡è´­å°è´¦ä¸å†æä¾›ã€Œæ˜¯å¦è´¨æ£€ã€é€‰æ‹©ï¼šé‡‡è´­æ˜Žç»†ä¸€å¾‹éœ€è¦æ¥æ–™æ£€éªŒ
            item.setQcCheckFlag(true);
            item.setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()));
            if (item.getTotalPrice() == null) {
                return;
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/arrivalnotice/MesArrivalNoticeApi.java
@@ -1,5 +1,10 @@
package cn.iocoder.yudao.module.mes.api.arrivalnotice;
import cn.iocoder.yudao.module.mes.api.arrivalnotice.dto.MesArrivalNoticeRespDTO;
import java.util.Collection;
import java.util.List;
/**
 * MES åˆ°è´§é€šçŸ¥å• API æŽ¥å£
 */
@@ -13,4 +18,12 @@
     */
    Long generateArrivalNoticeFromPurchaseOrder(Long purchaseOrderId);
    /**
     * æ ¹æ®é‡‡è´­è®¢å•编号批量查询到货通知单
     *
     * @param purchaseOrderIds é‡‡è´­è®¢å•编号集合
     * @return åˆ°è´§é€šçŸ¥å•列表
     */
    List<MesArrivalNoticeRespDTO> getArrivalNoticeListByPurchaseOrderIds(Collection<Long> purchaseOrderIds);
}
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/arrivalnotice/dto/MesArrivalNoticeRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.mes.api.arrivalnotice.dto;
import lombok.Data;
import java.time.LocalDateTime;
/**
 * MES åˆ°è´§é€šçŸ¥å• Response DTO
 */
@Data
public class MesArrivalNoticeRespDTO {
    private Long id;
    /**
     * åˆ°è´§é€šçŸ¥å•号
     */
    private String code;
    /**
     * åˆ°è´§é€šçŸ¥å•名称
     */
    private String name;
    /**
     * é‡‡è´­è®¢å•编号
     */
    private Long purchaseOrderId;
    /**
     * é‡‡è´­è®¢å•号
     */
    private String purchaseOrderCode;
    /**
     * çŠ¶æ€ï¼š0-草稿 2-待质检 3-待入库 4-已完成
     */
    private Integer status;
    /**
     * çŠ¶æ€åç§°
     */
    private String statusName;
    /**
     * åˆ°è´§æ—¥æœŸ
     */
    private LocalDateTime arrivalDate;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/arrivalnotice/MesArrivalNoticeApiImpl.java
@@ -1,8 +1,19 @@
package cn.iocoder.yudao.module.mes.api.arrivalnotice;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.api.arrivalnotice.dto.MesArrivalNoticeRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.arrivalnotice.MesWmArrivalNoticeMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmArrivalNoticeStatusEnum;
import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
/**
 * MES åˆ°è´§é€šçŸ¥å• API å®žçŽ°ç±»
@@ -12,10 +23,32 @@
    @Resource
    private MesWmArrivalNoticeService arrivalNoticeService;
    @Resource
    private MesWmArrivalNoticeMapper arrivalNoticeMapper;
    @Override
    public Long generateArrivalNoticeFromPurchaseOrder(Long purchaseOrderId) {
        return arrivalNoticeService.generateArrivalNoticeFromPurchaseOrder(purchaseOrderId);
    }
}
    @Override
    public List<MesArrivalNoticeRespDTO> getArrivalNoticeListByPurchaseOrderIds(Collection<Long> purchaseOrderIds) {
        if (CollUtil.isEmpty(purchaseOrderIds)) {
            return List.of();
        }
        List<MesWmArrivalNoticeDO> notices = arrivalNoticeMapper.selectListByPurchaseOrderIds(purchaseOrderIds);
        if (CollUtil.isEmpty(notices)) {
            return List.of();
        }
        return notices.stream().map(notice -> BeanUtils.toBean(notice, MesArrivalNoticeRespDTO.class,
                resp -> resp.setStatusName(resolveStatusName(notice.getStatus())))).toList();
    }
    private String resolveStatusName(Integer status) {
        return Arrays.stream(MesWmArrivalNoticeStatusEnum.values())
                .filter(item -> Objects.equals(item.getStatus(), status))
                .map(MesWmArrivalNoticeStatusEnum::getName)
                .findFirst().orElse(null);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/arrivalnotice/MesWmArrivalNoticeController.java
@@ -12,6 +12,8 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.arrivalnotice.vo.MesWmArrivalNoticeRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.arrivalnotice.vo.MesWmArrivalNoticeSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeLineDO;
import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeLineService;
import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeService;
import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi;
import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi;
@@ -45,6 +47,8 @@
    @Resource
    private MesWmArrivalNoticeService arrivalNoticeService;
    @Resource
    private MesWmArrivalNoticeLineService arrivalNoticeLineService;
    @Resource
    private SrmSupplierApi srmSupplierApi;
    @Resource
@@ -149,7 +153,11 @@
        Map<Long, ErpPurchaseOrderRespDTO> purchaseOrderMap = purchaseOrderApi.getPurchaseOrderList(purchaseOrderIds)
                .getCheckedData().stream()
                .collect(java.util.stream.Collectors.toMap(ErpPurchaseOrderRespDTO::getId, v -> v, (a, b) -> a));
        // 3. æž„建结果
        // 3. èŽ·å¾—è´¨æ£€è¿›åº¦ï¼šéœ€æ£€è¡Œ / å·²ç”Ÿæˆæ£€éªŒå•行 / å·²å®Œæˆæ£€éªŒè¡Œ
        Map<Long, List<MesWmArrivalNoticeLineDO>> lineMap = arrivalNoticeLineService
                .getArrivalNoticeLineListByNoticeIds(convertSet(list, MesWmArrivalNoticeDO::getId))
                .stream().collect(java.util.stream.Collectors.groupingBy(MesWmArrivalNoticeLineDO::getNoticeId));
        // 4. æž„建结果
        return BeanUtils.toBean(list, MesWmArrivalNoticeRespVO.class, vo -> {
            MapUtils.findAndThen(supplierMap, vo.getVendorId(), supplier ->
                    vo.setVendorName(supplier.getName()));
@@ -160,6 +168,14 @@
                    vo.setContractNo(purchaseOrder.getContractNo());
                }
            });
            List<MesWmArrivalNoticeLineDO> lines = lineMap.getOrDefault(vo.getId(), Collections.emptyList());
            List<MesWmArrivalNoticeLineDO> needCheckLines = lines.stream()
                    .filter(line -> Boolean.TRUE.equals(line.getIqcCheckFlag())).toList();
            vo.setIqcNeedCount(needCheckLines.size());
            vo.setIqcCreatedCount((int) needCheckLines.stream()
                    .filter(line -> line.getIqcId() != null).count());
            vo.setIqcInspectedCount((int) needCheckLines.stream()
                    .filter(line -> line.getIqcCheckResult() != null).count());
        });
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/arrivalnotice/vo/MesWmArrivalNoticeRespVO.java
@@ -70,4 +70,15 @@
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
    // ========== è´¨æ£€è¿›åº¦ï¼ˆåˆ—表页用于区分是否已生成来料检验单) ==========
    @Schema(description = "需检验行数(iqcCheckFlag=true çš„æ˜Žç»†è¡Œæ•°ï¼‰", example = "2")
    private Integer iqcNeedCount;
    @Schema(description = "已生成来料检验单的行数", example = "2")
    private Integer iqcCreatedCount;
    @Schema(description = "已完成检验(有检验结果)的行数", example = "0")
    private Integer iqcInspectedCount;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/arrivalnotice/MesWmArrivalNoticeLineMapper.java
@@ -7,6 +7,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeLineDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
@@ -25,6 +26,11 @@
        return selectList(MesWmArrivalNoticeLineDO::getNoticeId, noticeId);
    }
    default List<MesWmArrivalNoticeLineDO> selectListByNoticeIds(Collection<Long> noticeIds) {
        return selectList(new LambdaQueryWrapperX<MesWmArrivalNoticeLineDO>()
                .inIfPresent(MesWmArrivalNoticeLineDO::getNoticeId, noticeIds));
    }
    default void deleteByNoticeId(Long noticeId) {
        delete(MesWmArrivalNoticeLineDO::getNoticeId, noticeId);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/arrivalnotice/MesWmArrivalNoticeMapper.java
@@ -7,6 +7,9 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * MES åˆ°è´§é€šçŸ¥å• Mapper
 */
@@ -37,4 +40,9 @@
        return selectOne(MesWmArrivalNoticeDO::getPurchaseOrderId, purchaseOrderId);
    }
    default List<MesWmArrivalNoticeDO> selectListByPurchaseOrderIds(Collection<Long> purchaseOrderIds) {
        return selectList(new LambdaQueryWrapperX<MesWmArrivalNoticeDO>()
                .inIfPresent(MesWmArrivalNoticeDO::getPurchaseOrderId, purchaseOrderIds));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/iqc/MesQcIqcServiceImpl.java
@@ -112,7 +112,7 @@
        // 2. æ’入主表
        MesQcIqcDO iqc = BeanUtils.toBean(createReqVO, MesQcIqcDO.class)
                .setTemplateId(null)
                .setCheckQuantity(createReqVO.getQualifiedQuantity().add(createReqVO.getUnqualifiedQuantity()))
                .setCheckQuantity(resolveCheckQuantity(createReqVO.getQualifiedQuantity(), createReqVO.getUnqualifiedQuantity()))
                .setSourceDocCode(sourceDocCode)
                .setStatus(MesQcStatusEnum.DRAFT.getStatus());
        iqcMapper.insert(iqc);
@@ -131,8 +131,18 @@
        updateObj.setSourceDocType(null).setSourceDocId(null).setSourceLineId(null); // ä¸å…è®¸ä¿®æ”¹æ¥æºå•据
        updateObj.setTemplateId(null); // ä¸å…è®¸ä¿®æ”¹æ¨¡æ¿
        updateObj.setItemId(null); // ä¸å…è®¸ä¿®æ”¹ç‰©æ–™
        updateObj.setCheckQuantity(updateReqVO.getQualifiedQuantity().add(updateReqVO.getUnqualifiedQuantity()));
        updateObj.setCheckQuantity(resolveCheckQuantity(updateReqVO.getQualifiedQuantity(), updateReqVO.getUnqualifiedQuantity()));
        iqcMapper.updateById(updateObj);
    }
    /**
     * æ£€éªŒæ•°é‡ = åˆæ ¼æ•°é‡ + ä¸åˆæ ¼æ•°é‡ï¼›ä¸¤è€…未填全时留空,等检验员录入后再计算
     */
    private BigDecimal resolveCheckQuantity(BigDecimal qualified, BigDecimal unqualified) {
        if (qualified == null || unqualified == null) {
            return null;
        }
        return qualified.add(unqualified);
    }
    private void validateIqcSaveData(MesQcIqcSaveReqVO reqVO) {
@@ -393,9 +403,10 @@
        }
        iqc.setItemId(line.getItemId());
        iqc.setReceivedQuantity(line.getArrivalQuantity());
        iqc.setCheckQuantity(BigDecimal.ZERO); // å¾…填写
        iqc.setQualifiedQuantity(BigDecimal.ZERO); // å¾…填写
        iqc.setUnqualifiedQuantity(BigDecimal.ZERO); // å¾…填写
        // åˆæ ¼/不合格数量留空,由检验员填写时录入(不再默认 0,避免误判为「已检验合格 0」)
        iqc.setCheckQuantity(null);
        iqc.setQualifiedQuantity(null);
        iqc.setUnqualifiedQuantity(null);
        iqc.setReceiveDate(java.time.LocalDateTime.now());
        iqc.setStatus(MesQcStatusEnum.DRAFT.getStatus());
        iqcMapper.insert(iqc);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ncr/MesQcNcrPurchaseSourceValidator.java
@@ -43,7 +43,7 @@
                .eq(MesWmArrivalNoticeLineDO::getId, iqc.getSourceLineId()).last("FOR UPDATE"));
        if (notice == null || line == null || !Objects.equals(line.getNoticeId(), notice.getId())
                || !Objects.equals(line.getIqcId(), iqc.getId())
                || !Objects.equals(notice.getStatus(), MesWmArrivalNoticeStatusEnum.PENDING_QC.getStatus())
                || !isDispositionAllowedNoticeStatus(notice.getStatus())
                || !Objects.equals(line.getItemId(), iqc.getItemId())
                || !Objects.equals(ncr.getItemId(), iqc.getItemId())
                || !samePositive(ncr.getUnqualifiedQuantity(), iqc.getUnqualifiedQuantity())
@@ -52,6 +52,19 @@
        return new Source(iqc, notice, line);
    }
    /**
     * ä¸åˆæ ¼å¤„置允许的到货通知状态。
     *
     * æ¥æ–™æ£€éªŒå®Œæˆæ—¶ï¼Œåˆ°è´§é€šçŸ¥ä¼šåœ¨åŒä¸€äº‹åŠ¡é‡Œä»Žã€Œå¾…è´¨æ£€ã€æŽ¨è¿›åˆ°ã€Œå¾…å…¥åº“ã€å¹¶è‡ªåŠ¨ç”Ÿæˆé‡‡è´­å…¥åº“å•
     * ï¼ˆåªå–合格数量),随后确认入库后变为「已完成」。不合格部分的退货/让步由不合格单单独处置,
     * ä¸å†å—通知状态限制,否则单行通知(或最后一行检验完成时)的不合格单将永远无法处置。
     */
    private boolean isDispositionAllowedNoticeStatus(Integer status) {
        return Objects.equals(status, MesWmArrivalNoticeStatusEnum.PENDING_QC.getStatus())
                || Objects.equals(status, MesWmArrivalNoticeStatusEnum.PENDING_RECEIPT.getStatus())
                || Objects.equals(status, MesWmArrivalNoticeStatusEnum.FINISHED.getStatus());
    }
    private boolean samePositive(BigDecimal a, BigDecimal b) {
        return a != null && b != null && a.signum() > 0 && a.compareTo(b) == 0;
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ncr/MesQcNcrServiceImpl.java
@@ -287,7 +287,8 @@
            respVO.setDocId(doc.getId());
            respVO.setDocCode(doc.getCode());
            respVO.setDocName("采购退货单");
            respVO.setMessage("采购退货单已生成,请由仓库确认退货");
            respVO.setMessage("采购退货单 " + doc.getCode()
                    + " å·²ç”Ÿæˆï¼šä¸åˆæ ¼å“æœªå…¥åº“,无需拣货,单据已直接完成,可在【采购仓库-采购退货】查看");
        } else if (Objects.equals(disposeReqVO.getDisposition(), MesNcrDispositionEnum.REPRODUCTION.getType())) {
            respVO = processSource != null
                    ? oqcReproductionService.reproduceIpqc(ncr, disposeReqVO.getDisposition(), processSource)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ncr/MesQcPurchaseReturnService.java
@@ -5,8 +5,15 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ncr.MesQcNcrDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ncr.MesQcPurchaseReturnDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnvendor.MesWmReturnVendorDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnvendor.MesWmReturnVendorLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.ncr.MesQcPurchaseReturnMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.ncr.MesQcPurchaseConcessionMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.returnvendor.MesWmReturnVendorLineMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.returnvendor.MesWmReturnVendorMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmReturnVendorStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.hutool.core.util.StrUtil;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -16,10 +23,16 @@
@Service
public class MesQcPurchaseReturnService {
    /** é‡‡è´­é€€è´§å•编码规则(MesMdAutoCodeRule ä¸­çš„ code) */
    private static final String RETURN_VENDOR_CODE_RULE = "WM_RETURN_VENDOR_CODE";
    @Resource private MesQcNcrPurchaseSourceValidator sourceValidator;
    @Resource private MesQcPurchaseReturnMapper mapper;
    @Resource private MesQcPurchaseConcessionMapper concessionMapper;
    @Resource private MesQcArrivalDispositionService arrivalDispositionService;
    @Resource private MesWmReturnVendorMapper returnVendorMapper;
    @Resource private MesWmReturnVendorLineMapper returnVendorLineMapper;
    @Resource private MesMdAutoCodeRecordService autoCodeRecordService;
    /** å¿…须由已锁定 NCR çš„处置事务调用。 */
    @Transactional(rollbackFor = Exception.class)
@@ -32,8 +45,35 @@
        if (source.notice().getVendorId() == null || source.notice().getPurchaseOrderId() == null
                || source.line().getPurchaseOrderItemId() == null)
            throw new ServiceException(400, "到货单缺少供应商、采购订单或采购明细,无法生成可追溯退货单");
        // 1. ç”Ÿæˆã€Œä¾›åº”商退货单」:不合格品从未入库,不产生库存出库,也无需拣货与审批,
        //    ç›´æŽ¥ä»¥å·²å®ŒæˆçŠ¶æ€è½åˆ°é‡‡è´­ä»“åº“-采购退货列表中,供仓库/采购追溯并按实物退回供应商。
        String code = autoCodeRecordService.generateAutoCode(RETURN_VENDOR_CODE_RULE, null);
        MesWmReturnVendorDO vendorReturn = new MesWmReturnVendorDO();
        vendorReturn.setCode(code);
        vendorReturn.setName("来料不合格退货-" + ncr.getCode());
        vendorReturn.setPurchaseOrderId(source.notice().getPurchaseOrderId());
        vendorReturn.setPurchaseOrderCode(source.notice().getPurchaseOrderCode());
        vendorReturn.setContractNo(source.notice().getContractNo());
        vendorReturn.setVendorId(source.notice().getVendorId());
        vendorReturn.setReturnDate(LocalDateTime.now());
        vendorReturn.setReturnReason("来料检验不合格");
        vendorReturn.setStatus(MesWmReturnVendorStatusEnum.FINISHED.getStatus());
        vendorReturn.setRemark("由来料不合格单 " + ncr.getCode() + "(来料检验 " + source.iqc().getCode()
                + ")退货处置自动生成;不合格品未入库,无需拣货,单据直接完成。");
        returnVendorMapper.insert(vendorReturn);
        MesWmReturnVendorLineDO vendorReturnLine = new MesWmReturnVendorLineDO();
        vendorReturnLine.setReturnId(vendorReturn.getId());
        vendorReturnLine.setItemId(ncr.getItemId());
        vendorReturnLine.setQuantity(ncr.getUnqualifiedQuantity());
        vendorReturnLine.setBatchCode(StrUtil.blankToDefault(source.iqc().getVendorBatch(), null));
        vendorReturnLine.setRemark("来料检验 " + source.iqc().getCode() + " ä¸åˆæ ¼æ•°é‡");
        returnVendorLineMapper.insert(vendorReturnLine);
        // 2. è®°å½•质量侧处置台账(与退货单同号),因退货单已直接完成,这里同步标记为已确认,
        //    ä¸åˆæ ¼å•关闭时校验的正是这条记录。
        MesQcPurchaseReturnDO doc = new MesQcPurchaseReturnDO();
        doc.setCode("NCR-RET-" + ncr.getId());
        doc.setCode(code);
        doc.setNcrId(ncr.getId());
        doc.setIqcId(ncr.getQcId());
        doc.setNoticeId(source.notice().getId());
@@ -44,8 +84,12 @@
        doc.setItemId(ncr.getItemId());
        doc.setVendorBatch(source.iqc().getVendorBatch());
        doc.setQuantity(ncr.getUnqualifiedQuantity());
        doc.setStatus(0);
        doc.setStatus(1);
        doc.setConfirmedBy(getLoginUserId());
        doc.setConfirmedAt(LocalDateTime.now());
        doc.setConfirmationRemark("随不合格处置自动生成采购退货单 " + code + ",未发生库存出库");
        mapper.insert(doc);
        arrivalDispositionService.refresh(source.notice().getId());
        return doc;
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java
@@ -368,9 +368,10 @@
        oqc.setBatchCode(line.getBatchCode());
        oqc.setReelNo(resolveSalesReelNo(line));
        oqc.setOutQuantity(line.getQuantity());
        oqc.setCheckQuantity(BigDecimal.ZERO); // å¾…填写
        oqc.setQualifiedQuantity(BigDecimal.ZERO); // å¾…填写
        oqc.setUnqualifiedQuantity(BigDecimal.ZERO); // å¾…填写
        // æ£€æµ‹/合格/不合格数量留空,由检验员填写时录入(不再默认 0)
        oqc.setCheckQuantity(null);
        oqc.setQualifiedQuantity(null);
        oqc.setUnqualifiedQuantity(null);
        MesWmProductSalesDO sales = productSalesService.getProductSales(salesId);
        java.time.LocalDateTime businessDate = sales != null && sales.getSalesDate() != null
                ? sales.getSalesDate() : java.time.LocalDateTime.now();
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeLineService.java
@@ -7,6 +7,7 @@
import jakarta.validation.Valid;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
/**
@@ -61,6 +62,14 @@
    List<MesWmArrivalNoticeLineDO> getArrivalNoticeLineListByNoticeId(Long noticeId);
    /**
     * æŒ‰é€šçŸ¥å•编号集合批量获得行列表(列表页一次性查询,避免逐单查询)
     *
     * @param noticeIds é€šçŸ¥å•编号集合
     * @return è¡Œåˆ—表
     */
    List<MesWmArrivalNoticeLineDO> getArrivalNoticeLineListByNoticeIds(Collection<Long> noticeIds);
    /**
     * æŒ‰é€šçŸ¥å•编号批量删除行
     *
     * @param noticeId é€šçŸ¥å•编号
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeLineServiceImpl.java
@@ -15,6 +15,7 @@
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -107,6 +108,11 @@
    }
    @Override
    public List<MesWmArrivalNoticeLineDO> getArrivalNoticeLineListByNoticeIds(Collection<Long> noticeIds) {
        return arrivalNoticeLineMapper.selectListByNoticeIds(noticeIds);
    }
    @Override
    public void deleteArrivalNoticeLineByNoticeId(Long noticeId) {
        arrivalNoticeLineMapper.deleteByNoticeId(noticeId);
    }