浏览代码

✨ feat(stock): 添加虚拟仓过滤功能

新增虚拟仓过滤模式,支持排除虚拟仓和仅查看虚拟仓的选项。更新相关请求对象和数据查询逻辑,以便后端处理虚拟仓的过滤需求。
YunaiV 2 月之前
父节点
当前提交
a693d31e23

+ 12 - 0
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockPageReqVO.java

@@ -12,6 +12,18 @@ import lombok.ToString;
 @ToString(callSuper = true)
 public class MesWmMaterialStockPageReqVO extends PageParam {
 
+    /**
+     * 虚拟仓过滤模式 - 排除虚拟仓
+     */
+    public static final String VIRTUAL_FILTER_EXCLUDE = "exclude";
+    /**
+     * 虚拟仓过滤模式 - 只看虚拟仓
+     */
+    public static final String VIRTUAL_FILTER_ONLY = "only";
+
+    @Schema(description = "虚拟仓过滤模式", example = "exclude")
+    private String virtualFilter;
+
     @Schema(description = "物料分类编号", example = "1")
     private Long itemTypeId;
 

+ 15 - 5
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/materialstock/MesWmMaterialStockMapper.java

@@ -21,8 +21,9 @@ public interface MesWmMaterialStockMapper extends BaseMapperX<MesWmMaterialStock
 
     default PageResult<MesWmMaterialStockDO> selectPage(MesWmMaterialStockPageReqVO reqVO,
                                                          Collection<Long> itemTypeIds,
-                                                         Collection<Long> itemIds) {
-        return selectPage(reqVO, new LambdaQueryWrapperX<MesWmMaterialStockDO>()
+                                                         Collection<Long> itemIds,
+                                                         Long virtualWarehouseId) {
+        LambdaQueryWrapperX<MesWmMaterialStockDO> wrapper = new LambdaQueryWrapperX<MesWmMaterialStockDO>()
                 .inIfPresent(MesWmMaterialStockDO::getItemTypeId, itemTypeIds)
                 .inIfPresent(MesWmMaterialStockDO::getItemId, itemIds)
                 .likeIfPresent(MesWmMaterialStockDO::getBatchCode, reqVO.getBatchCode())
@@ -31,9 +32,18 @@ public interface MesWmMaterialStockMapper extends BaseMapperX<MesWmMaterialStock
                 .eqIfPresent(MesWmMaterialStockDO::getLocationId, reqVO.getLocationId())
                 .eqIfPresent(MesWmMaterialStockDO::getAreaId, reqVO.getAreaId())
                 .eqIfPresent(MesWmMaterialStockDO::getVendorId, reqVO.getVendorId())
-                .eqIfPresent(MesWmMaterialStockDO::getFrozen, reqVO.getFrozen())
-                .ne(MesWmMaterialStockDO::getQuantity, BigDecimal.ZERO)
-                .orderByAsc(MesWmMaterialStockDO::getReceiptTime));
+                .eqIfPresent(MesWmMaterialStockDO::getFrozen, reqVO.getFrozen());
+        wrapper.ne(MesWmMaterialStockDO::getQuantity, BigDecimal.ZERO)
+                .orderByAsc(MesWmMaterialStockDO::getReceiptTime);
+        // 虚拟仓过滤(Service 层已将 virtualFilter 解析为 virtualWarehouseId)
+        if (virtualWarehouseId != null) {
+            if (MesWmMaterialStockPageReqVO.VIRTUAL_FILTER_ONLY.equals(reqVO.getVirtualFilter())) {
+                wrapper.eq(MesWmMaterialStockDO::getWarehouseId, virtualWarehouseId);
+            } else if (MesWmMaterialStockPageReqVO.VIRTUAL_FILTER_EXCLUDE.equals(reqVO.getVirtualFilter())) {
+                wrapper.ne(MesWmMaterialStockDO::getWarehouseId, virtualWarehouseId);
+            }
+        }
+        return selectPage(reqVO, wrapper);
     }
 
     default Long selectCountByWarehouseId(Long warehouseId) {

+ 17 - 1
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.mes.service.wm.materialstock;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
@@ -11,11 +12,14 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
 import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper;
 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService;
 import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
+import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
 import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
@@ -43,6 +47,9 @@ public class MesWmMaterialStockServiceImpl implements MesWmMaterialStockService
     private MesMdItemTypeService itemTypeService;
     @Resource
     private MesWmWarehouseAreaService areaService;
+    @Resource
+    @Lazy
+    private MesWmWarehouseService warehouseService;
 
     @Override
     public MesWmMaterialStockDO getMaterialStock(Long id) {
@@ -77,9 +84,18 @@ public class MesWmMaterialStockServiceImpl implements MesWmMaterialStockService
         if (pageReqVO.getItemId() != null) {
             itemIds = SetUtils.asSet(pageReqVO.getItemId());
         }
+        // 1.3 解析 virtualFilter:转换为虚拟仓 warehouseId
+        Long virtualWarehouseId = null;
+        String virtualFilter = pageReqVO.getVirtualFilter();
+        if (MesWmMaterialStockPageReqVO.VIRTUAL_FILTER_EXCLUDE.equals(virtualFilter)
+                || MesWmMaterialStockPageReqVO.VIRTUAL_FILTER_ONLY.equals(virtualFilter)) {
+            MesWmWarehouseDO virtualWarehouse = warehouseService.getWarehouseByCode(
+                    MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE);
+            Assert.notNull(virtualWarehouse, "虚拟仓库(WIP_VIRTUAL_WAREHOUSE)不存在");
+        }
 
         // 2. 分页查询
-        return materialStockMapper.selectPage(pageReqVO, itemTypeIds, itemIds);
+        return materialStockMapper.selectPage(pageReqVO, itemTypeIds, itemIds, virtualWarehouseId);
     }
 
     @Override

+ 2 - 0
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesDetailServiceImpl.java

@@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
 import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
 import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
 import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
@@ -33,6 +34,7 @@ public class MesWmProductSalesDetailServiceImpl implements MesWmProductSalesDeta
     @Resource
     private MesMdItemService itemService;
     @Resource
+    @Lazy
     private MesWmProductSalesLineService productSalesLineService;
     @Resource
     private MesWmMaterialStockService materialStockService;

+ 220 - 0
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/productsales/MesWmProductSalesDetailMapperTest.java

@@ -0,0 +1,220 @@
+package cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDetailDO;
+import jakarta.annotation.Resource;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.function.Consumer;
+
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link MesWmProductSalesDetailMapper} 的单元测试
+ * <p>
+ * 覆盖:selectListByLineId、selectListBySalesId、deleteByLineId、deleteBySalesId
+ */
+public class MesWmProductSalesDetailMapperTest extends BaseDbUnitTest {
+
+    @Resource
+    private MesWmProductSalesDetailMapper detailMapper;
+
+    // ==================== 辅助方法 ====================
+
+    /**
+     * 创建明细 DO,固定 BigDecimal 精度为 2 位,避免 H2 decimal(14,2) 精度不匹配
+     */
+    private MesWmProductSalesDetailDO createDetailPojo(Consumer<MesWmProductSalesDetailDO> consumer) {
+        return randomPojo(MesWmProductSalesDetailDO.class, o -> {
+            o.setQuantity(new BigDecimal("10.00"));
+            consumer.accept(o);
+        });
+    }
+
+    // ==================== selectListByLineId ====================
+
+    @Test
+    public void testSelectListByLineId_match() {
+        Long lineId = 100L;
+        MesWmProductSalesDetailDO match = createDetailPojo(o -> {
+            o.setLineId(lineId);
+            o.setSalesId(1L);
+        });
+        detailMapper.insert(match);
+        // 插入另一条不同 lineId 的记录,不应被返回
+        detailMapper.insert(cloneIgnoreId(match, o -> o.setLineId(999L)));
+
+        List<MesWmProductSalesDetailDO> result = detailMapper.selectListByLineId(lineId);
+
+        assertEquals(1, result.size());
+        assertPojoEquals(match, result.get(0));
+    }
+
+    @Test
+    public void testSelectListByLineId_multipleRows() {
+        Long lineId = 200L;
+        MesWmProductSalesDetailDO detail1 = createDetailPojo(o -> {
+            o.setLineId(lineId);
+            o.setSalesId(2L);
+        });
+        MesWmProductSalesDetailDO detail2 = createDetailPojo(o -> {
+            o.setLineId(lineId);
+            o.setSalesId(2L);
+        });
+        detailMapper.insert(detail1);
+        detailMapper.insert(detail2);
+
+        List<MesWmProductSalesDetailDO> result = detailMapper.selectListByLineId(lineId);
+
+        assertEquals(2, result.size());
+    }
+
+    @Test
+    public void testSelectListByLineId_noMatch() {
+        MesWmProductSalesDetailDO detail = createDetailPojo(o -> {
+            o.setLineId(300L);
+            o.setSalesId(3L);
+        });
+        detailMapper.insert(detail);
+
+        List<MesWmProductSalesDetailDO> result = detailMapper.selectListByLineId(999L);
+
+        assertTrue(result.isEmpty());
+    }
+
+    // ==================== selectListBySalesId ====================
+
+    @Test
+    public void testSelectListBySalesId_match() {
+        Long salesId = 10L;
+        MesWmProductSalesDetailDO match = createDetailPojo(o -> {
+            o.setSalesId(salesId);
+            o.setLineId(1L);
+        });
+        detailMapper.insert(match);
+        // 插入另一条不同 salesId 的记录
+        detailMapper.insert(cloneIgnoreId(match, o -> o.setSalesId(888L)));
+
+        List<MesWmProductSalesDetailDO> result = detailMapper.selectListBySalesId(salesId);
+
+        assertEquals(1, result.size());
+        assertPojoEquals(match, result.get(0));
+    }
+
+    @Test
+    public void testSelectListBySalesId_multipleRows() {
+        Long salesId = 20L;
+        MesWmProductSalesDetailDO detail1 = createDetailPojo(o -> {
+            o.setSalesId(salesId);
+            o.setLineId(11L);
+        });
+        MesWmProductSalesDetailDO detail2 = createDetailPojo(o -> {
+            o.setSalesId(salesId);
+            o.setLineId(12L);
+        });
+        detailMapper.insert(detail1);
+        detailMapper.insert(detail2);
+
+        List<MesWmProductSalesDetailDO> result = detailMapper.selectListBySalesId(salesId);
+
+        assertEquals(2, result.size());
+    }
+
+    @Test
+    public void testSelectListBySalesId_noMatch() {
+        MesWmProductSalesDetailDO detail = createDetailPojo(o -> {
+            o.setSalesId(30L);
+            o.setLineId(2L);
+        });
+        detailMapper.insert(detail);
+
+        List<MesWmProductSalesDetailDO> result = detailMapper.selectListBySalesId(9999L);
+
+        assertTrue(result.isEmpty());
+    }
+
+    // ==================== deleteByLineId ====================
+
+    @Test
+    public void testDeleteByLineId_deletesOnlyTargetLine() {
+        Long lineId = 400L;
+        MesWmProductSalesDetailDO toDelete = createDetailPojo(o -> {
+            o.setLineId(lineId);
+            o.setSalesId(4L);
+        });
+        MesWmProductSalesDetailDO toKeep = createDetailPojo(o -> {
+            o.setLineId(500L);
+            o.setSalesId(4L);
+        });
+        detailMapper.insert(toDelete);
+        detailMapper.insert(toKeep);
+
+        detailMapper.deleteByLineId(lineId);
+
+        // lineId=400 的记录已被逻辑删除
+        List<MesWmProductSalesDetailDO> deletedResult = detailMapper.selectListByLineId(lineId);
+        assertTrue(deletedResult.isEmpty(), "lineId=400 的记录应被删除");
+
+        // lineId=500 的记录保留
+        List<MesWmProductSalesDetailDO> keptResult = detailMapper.selectListByLineId(500L);
+        assertEquals(1, keptResult.size());
+    }
+
+    @Test
+    public void testDeleteByLineId_multipleRows() {
+        Long lineId = 600L;
+        detailMapper.insert(createDetailPojo(o -> { o.setLineId(lineId); o.setSalesId(5L); }));
+        detailMapper.insert(createDetailPojo(o -> { o.setLineId(lineId); o.setSalesId(5L); }));
+
+        detailMapper.deleteByLineId(lineId);
+
+        List<MesWmProductSalesDetailDO> result = detailMapper.selectListByLineId(lineId);
+        assertTrue(result.isEmpty(), "同 lineId 下的所有记录均应被删除");
+    }
+
+    // ==================== deleteBySalesId ====================
+
+    @Test
+    public void testDeleteBySalesId_deletesOnlyTargetSales() {
+        Long salesId = 700L;
+        MesWmProductSalesDetailDO toDelete = createDetailPojo(o -> {
+            o.setSalesId(salesId);
+            o.setLineId(6L);
+        });
+        MesWmProductSalesDetailDO toKeep = createDetailPojo(o -> {
+            o.setSalesId(800L);
+            o.setLineId(7L);
+        });
+        detailMapper.insert(toDelete);
+        detailMapper.insert(toKeep);
+
+        detailMapper.deleteBySalesId(salesId);
+
+        // salesId=700 的记录已被逻辑删除
+        List<MesWmProductSalesDetailDO> deletedResult = detailMapper.selectListBySalesId(salesId);
+        assertTrue(deletedResult.isEmpty(), "salesId=700 的记录应被删除");
+
+        // salesId=800 的记录保留
+        List<MesWmProductSalesDetailDO> keptResult = detailMapper.selectListBySalesId(800L);
+        assertEquals(1, keptResult.size());
+    }
+
+    @Test
+    public void testDeleteBySalesId_multipleRows() {
+        Long salesId = 900L;
+        detailMapper.insert(createDetailPojo(o -> { o.setSalesId(salesId); o.setLineId(8L); }));
+        detailMapper.insert(createDetailPojo(o -> { o.setSalesId(salesId); o.setLineId(9L); }));
+        detailMapper.insert(createDetailPojo(o -> { o.setSalesId(salesId); o.setLineId(10L); }));
+
+        detailMapper.deleteBySalesId(salesId);
+
+        List<MesWmProductSalesDetailDO> result = detailMapper.selectListBySalesId(salesId);
+        assertTrue(result.isEmpty(), "同 salesId 下的所有记录均应被删除");
+    }
+
+}

+ 25 - 0
yudao-module-mes/src/test/resources/sql/create_tables.sql

@@ -678,3 +678,28 @@ CREATE TABLE IF NOT EXISTS "mes_pro_route_process" (
     "tenant_id" bigint NOT NULL DEFAULT 0,
     PRIMARY KEY ("id")
 );
+
+-- ----------------------------
+-- MES 销售出库明细
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS "mes_wm_product_sales_detail" (
+    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "line_id" bigint NOT NULL,
+    "sales_id" bigint NOT NULL,
+    "item_id" bigint NOT NULL,
+    "quantity" decimal(14,2) DEFAULT NULL,
+    "material_stock_id" bigint DEFAULT NULL,
+    "batch_id" bigint DEFAULT NULL,
+    "batch_code" varchar(255) DEFAULT NULL,
+    "warehouse_id" bigint DEFAULT NULL,
+    "location_id" bigint DEFAULT NULL,
+    "area_id" bigint DEFAULT NULL,
+    "remark" varchar(500) DEFAULT NULL,
+    "creator" varchar(64) DEFAULT '',
+    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater" varchar(64) DEFAULT '',
+    "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "deleted" bit NOT NULL DEFAULT FALSE,
+    "tenant_id" bigint NOT NULL DEFAULT 0,
+    PRIMARY KEY ("id")
+);