云
2026-10-29 a1a94ab39a3ac9cc28601e3381b3b4fbc0cda02a
fix(mes): 解决产品收货批次信息传递和销售订单校验问题

- 修复产品收货服务中批次ID和批次代码从收货行获取而非明细
- 添加收货行存在性和归属校验逻辑
- 在产品销售行创建时添加销售订单明细校验
- 添加ERP销售订单API集成以验证销售订单项目关联
- 修复退货销售库存操作中的数量比较逻辑
- 添加退货销售批次信息在库存交易中的传递
- 增加相关单元测试验证销售订单校验和批次信息处理
已添加1个文件
已修改5个文件
133 ■■■■■ 文件已修改
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productsales/vo/line/MesWmProductSalesLineSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productreceipt/MesWmProductReceiptServiceImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesLineServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesLineReelTest.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesStockTest.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productsales/vo/line/MesWmProductSalesLineSaveReqVO.java
@@ -20,6 +20,9 @@
    @Schema(description = "发货通知单行ID", example = "1")
    private Long noticeLineId;
    @Schema(description = "销售订单明细ID")
    private Long saleOrderItemId;
    @Schema(description = "物料ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "物料不能为空")
    private Long itemId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productreceipt/MesWmProductReceiptServiceImpl.java
@@ -188,11 +188,18 @@
        // 2. éåŽ†æ˜Žç»†ï¼Œæ¯æ¡æ˜Žç»†äº§ç”Ÿ OUT(虚拟线边库扣减)+ IN(实际仓库增加)
        for (MesWmProductReceiptDetailDO detail : details) {
            MesWmProductReceiptLineDO line = productReceiptLineService.getProductReceiptLine(detail.getLineId());
            if (line == null || !receipt.getId().equals(line.getReceiptId())
                    || !detail.getItemId().equals(line.getItemId())) {
                throw exception(WM_PRODUCT_RECPT_NO_LINE);
            }
            Long batchId = line.getBatchId();
            String batchCode = line.getBatchCode();
            // 2.1 å…ˆä»Žè™šæ‹Ÿçº¿è¾¹åº“出库(库存减少)
            Long outTransactionId = wmTransactionService.createTransaction(new MesWmTransactionSaveReqDTO()
                    .setType(MesWmTransactionTypeEnum.OUT.getType()).setItemId(detail.getItemId())
                    .setQuantity(detail.getQuantity().negate()) // åº“存减少
                    .setBatchId(detail.getBatchId())
                    .setBatchId(batchId).setBatchCode(batchCode)
                    .setWarehouseId(virtualWarehouse.getId()).setLocationId(virtualLocation.getId()).setAreaId(virtualArea.getId())
                    .setCheckFlag(false) // çº¿è¾¹åº“允许负库存
                    .setBizType(MesBizTypeConstants.WM_PRODUCT_RECPT).setBizId(receipt.getId())
@@ -201,7 +208,7 @@
            wmTransactionService.createTransaction(new MesWmTransactionSaveReqDTO()
                    .setType(MesWmTransactionTypeEnum.IN.getType()).setItemId(detail.getItemId())
                    .setQuantity(detail.getQuantity()) // åº“存增加
                    .setBatchId(detail.getBatchId())
                    .setBatchId(batchId).setBatchCode(batchCode)
                    .setWarehouseId(detail.getWarehouseId()).setLocationId(detail.getLocationId()).setAreaId(detail.getAreaId())
                    .setBizType(MesBizTypeConstants.WM_PRODUCT_RECPT).setBizId(receipt.getId())
                    .setBizCode(receipt.getCode()).setBizLineId(detail.getLineId())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesLineServiceImpl.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mes.service.wm.productsales;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ObjUtil;
@@ -60,6 +61,8 @@
    @Resource
    @Lazy
    private MesQcOqcService oqcService;
    @Resource
    private ErpSaleOrderApi saleOrderApi;
    @Override
    public Long createProductSalesLine(MesWmProductSalesLineSaveReqVO createReqVO) {
@@ -196,6 +199,16 @@
        mdmItemService.validateItemExistsAndEnable(reqVO.getItemId());
        // æ ¡éªŒå…³è”发货通知单行(存在性 + å½’属 + å­—段一致性)
        MesWmSalesNoticeLineDO noticeLine = validateSalesNoticeLine(sales, reqVO);
        if (noticeLine == null && sales.getSaleOrderId() != null) {
            if (reqVO.getSaleOrderItemId() == null) {
                throw exception(WM_SALES_NOTICE_SALE_ORDER_ITEM_EMPTY);
            }
            var item = saleOrderApi.getSaleOrderItem(reqVO.getSaleOrderItemId()).getCheckedData();
            if (item == null || !Objects.equals(item.getOrderId(), sales.getSaleOrderId())
                    || !Objects.equals(item.getProductId(), reqVO.getItemId())) {
                throw exception(WM_SALES_NOTICE_LINE_SALE_ITEM_MISMATCH);
            }
        }
        // æ ¹æ® batchCode è§£æž batchId
        fillBatchId(reqVO);
        return noticeLine;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesServiceImpl.java
@@ -169,7 +169,7 @@
                BigDecimal totalDetailQuantity = CollectionUtils.getSumValue(details,
                        MesWmReturnSalesDetailDO::getQuantity, BigDecimal::add, BigDecimal.ZERO);
                // å¯¹æ¯”行数量与明细总数量,不满足直接抛出
                if (line.getQuantity().compareTo(totalDetailQuantity) > 0) {
                if (line.getQuantity() == null || line.getQuantity().compareTo(totalDetailQuantity) != 0) {
                    MesMdmItemDO item = mdmItemService.validateItemExistsAndEnable(line.getItemId());
                    throw exception(WM_RETURN_SALES_DETAIL_QUANTITY_MISMATCH,
                            item.getCode() + " " + item.getName() + " æœªå®Œæˆä¸Šæž¶");
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesLineReelTest.java
@@ -7,6 +7,9 @@
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesLineMapper;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.qc.oqc.MesQcOqcService;
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderItemRespDTO;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mes.service.wm.salesnotice.MesWmSalesNoticeLineService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -18,6 +21,7 @@
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@@ -29,6 +33,44 @@
    @Mock private MesWmSalesNoticeLineService noticeService;
    @Mock private MesMdmItemService itemService;
    @Mock private MesQcOqcService oqcService;
    @Mock private ErpSaleOrderApi saleOrderApi;
    @Test
    void directSaleRetainsSelectedOrderItem() {
        var req = new MesWmProductSalesLineSaveReqVO();
        req.setSalesId(1L);
        req.setSaleOrderItemId(5L);
        req.setItemId(3L);
        req.setQuantity(BigDecimal.ONE);
        when(salesService.validateProductSalesExistsAndDraft(1L))
                .thenReturn(MesWmProductSalesDO.builder().id(1L).saleOrderId(4L).build());
        var item = new ErpSaleOrderItemRespDTO();
        item.setId(5L);
        item.setOrderId(4L);
        item.setProductId(3L);
        when(saleOrderApi.getSaleOrderItem(5L)).thenReturn(CommonResult.success(item));
        service.createProductSalesLine(req);
        var line = ArgumentCaptor.forClass(MesWmProductSalesLineDO.class);
        verify(mapper).insert(line.capture());
        assertEquals(5L, line.getValue().getSaleOrderItemId());
    }
    @Test
    void directSaleRejectsItemFromAnotherOrder() {
        var req = new MesWmProductSalesLineSaveReqVO();
        req.setSalesId(1L);
        req.setSaleOrderItemId(5L);
        req.setItemId(3L);
        req.setQuantity(BigDecimal.ONE);
        when(salesService.validateProductSalesExistsAndDraft(1L))
                .thenReturn(MesWmProductSalesDO.builder().id(1L).saleOrderId(4L).build());
        var item = new ErpSaleOrderItemRespDTO();
        item.setOrderId(99L);
        item.setProductId(3L);
        when(saleOrderApi.getSaleOrderItem(5L)).thenReturn(CommonResult.success(item));
        assertThrows(RuntimeException.class, () -> service.createProductSalesLine(req));
        verify(mapper, never()).insert(any(MesWmProductSalesLineDO.class));
    }
    private MesWmProductSalesLineSaveReqVO request() {
        var req = new MesWmProductSalesLineSaveReqVO();
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesStockTest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.mes.service.wm.returnsales;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.returnsales.MesWmReturnSalesMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmReturnSalesStatusEnum;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class MesWmReturnSalesStockTest {
    @InjectMocks private MesWmReturnSalesServiceImpl service;
    @Mock private MesWmReturnSalesMapper mapper;
    @Mock private MesWmReturnSalesLineService lineService;
    @Mock private MesWmReturnSalesDetailService detailService;
    @Mock private MesMdmItemService itemService;
    @Mock private MesWmTransactionService transactionService;
    private void setup(BigDecimal quantity) {
        when(mapper.selectById(1L)).thenReturn(MesWmReturnSalesDO.builder().id(1L).code("TEST")
                .status(MesWmReturnSalesStatusEnum.APPROVED.getStatus()).build());
        when(lineService.getReturnSalesLineListByReturnId(1L)).thenReturn(List.of(
                MesWmReturnSalesLineDO.builder().id(2L).itemId(3L).quantity(BigDecimal.ONE).build()));
        when(detailService.getReturnSalesDetailListByReturnId(1L)).thenReturn(List.of(
                MesWmReturnSalesDetailDO.builder().lineId(2L).itemId(3L).quantity(quantity)
                        .batchId(4L).batchCode("TEST-BATCH").warehouseId(5L).build()));
    }
    @Test void rejectsExcessStocking() {
        setup(BigDecimal.TWO);
        when(itemService.validateItemExistsAndEnable(3L)).thenReturn(MesMdmItemDO.builder().code("TEST").name("test").build());
        assertThrows(ServiceException.class, () -> service.stockReturnSales(1L));
        verifyNoInteractions(transactionService);
    }
    @Test void preservesBatchOnStockTransaction() {
        setup(BigDecimal.ONE);
        service.stockReturnSales(1L);
        ArgumentCaptor<List<MesWmTransactionSaveReqDTO>> captor = ArgumentCaptor.forClass(List.class);
        verify(transactionService).createTransactionList(captor.capture());
        assertEquals(4L, captor.getValue().get(0).getBatchId());
        assertEquals("TEST-BATCH", captor.getValue().get(0).getBatchCode());
        assertEquals(BigDecimal.ONE, captor.getValue().get(0).getQuantity());
    }
}