test: verify dense outbound dispatch workflows
This commit is contained in:
@@ -0,0 +1,135 @@
|
|||||||
|
# 密集库出库、移库与 WCS 下发验收报告
|
||||||
|
|
||||||
|
## 验收结论
|
||||||
|
|
||||||
|
本次实现已完成代码级回归、生产构建、数据库迁移幂等验证、真实只读接口验证和浏览器页面验证。后端共执行 235 个测试,失败、错误和跳过均为 0;后端打包和前端生产构建均成功。
|
||||||
|
|
||||||
|
运行期验证使用本地 FTC25009-WMS 数据库和重新打包启动的后端,仅执行查询、预览、登录和页面访问,没有提交出库或移库作业,避免影响现有库存。
|
||||||
|
|
||||||
|
## 验证环境
|
||||||
|
|
||||||
|
- 日期:2026-07-16
|
||||||
|
- 分支:`codex/dense-outbound-relocation`
|
||||||
|
- Java:17
|
||||||
|
- Maven:`D:\BDevTools\maven\apache-maven-3.9.11\bin\mvn.cmd`
|
||||||
|
- 后端:`http://127.0.0.1:8080`
|
||||||
|
- 前端:`http://127.0.0.1`
|
||||||
|
- 数据库:FTC25009-WMS
|
||||||
|
|
||||||
|
## 自动化测试与构建
|
||||||
|
|
||||||
|
### 后端全量测试
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& 'D:\BDevTools\maven\apache-maven-3.9.11\bin\mvn.cmd' -pl ruoyi-admin -am test
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:`BUILD SUCCESS`,`Tests run: 235, Failures: 0, Errors: 0, Skipped: 0`。
|
||||||
|
|
||||||
|
覆盖的核心测试包括:
|
||||||
|
|
||||||
|
- `OutboundDispatchServiceImplTest`:初次失败、最多 5 次自动重试、人工下发、并发抢占、密集库阻塞和类型路由。
|
||||||
|
- `DenseLaneServiceImplTest`、`DenseLaneMapperSqlTest`:端头顺序、遮挡集合、同层不同通道目标、显式字段映射。
|
||||||
|
- `RelocationJobServiceImplTest`、`RelocationJobMapperSqlTest`:类型 23 作业、准确预览汇总、库存搬移、货位切换、归档与真实供应商字段。
|
||||||
|
- `OutStockoutServiceImplTest`、`LocationOutboundServiceImplTest`:指定库存和指定货位遇到遮挡时的移库编排。
|
||||||
|
- `KpiDenseRelocationServiceTest`:03 仓、同层目标、容量、预览与生成复核。
|
||||||
|
- `DenseLaneOutboundServiceImplTest`:端头 N 托、完整库存明细、准确货位汇总、预览签名失效和出库口适配。
|
||||||
|
- `WcsClientTest`、`OutMydoJobDispatchControllerTest`:独立 WCS 地址和人工下发接口。
|
||||||
|
|
||||||
|
### 后端打包
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& 'D:\BDevTools\maven\apache-maven-3.9.11\bin\mvn.cmd' clean package -DskipTests
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:`BUILD SUCCESS`。打包前停止了占用 JAR 的本地后端,完成后使用新 JAR 重新启动,并通过 `/captchaImage` 返回 HTTP 200 验证服务恢复。
|
||||||
|
|
||||||
|
### 前端生产构建
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-Location ruoyi-ui
|
||||||
|
npm run build:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:构建成功并生成 `dist`。存在项目原有的包体积提示,没有编译错误。
|
||||||
|
|
||||||
|
## 数据库迁移
|
||||||
|
|
||||||
|
连续执行两次:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& '.\scripts\apply-sql-file.ps1' -SqlFile 'sql\20260716_dense_outbound_relocation_dispatch.sql'
|
||||||
|
& '.\scripts\apply-sql-file.ps1' -SqlFile 'sql\20260716_dense_outbound_relocation_dispatch.sql'
|
||||||
|
```
|
||||||
|
|
||||||
|
两次均成功。执行后计数如下:
|
||||||
|
|
||||||
|
| 对象 | 数量 |
|
||||||
|
| --- | ---: |
|
||||||
|
| `wcs_job_dispatch` 表 | 1 |
|
||||||
|
| `IX_wcs_job_dispatch_retry` 索引 | 1 |
|
||||||
|
| `IX_mydo_job_dense_dispatch` 索引 | 1 |
|
||||||
|
| `bas_task_no` 类型 23 | 1 |
|
||||||
|
| WCS 补偿 Quartz 任务 | 1 |
|
||||||
|
| 指定通道出库菜单权限 | 1 |
|
||||||
|
| 通道出库预览权限 | 1 |
|
||||||
|
| 通道出库生成权限 | 1 |
|
||||||
|
| 出库作业人工下发权限 | 1 |
|
||||||
|
|
||||||
|
## 真实接口验证
|
||||||
|
|
||||||
|
在新打包后端上登录并调用只读接口,得到以下结果:
|
||||||
|
|
||||||
|
| 项目 | 结果 |
|
||||||
|
| --- | --- |
|
||||||
|
| 03 仓指定通道列表 | 12 条 |
|
||||||
|
| 通道编码为空的记录 | 0 条 |
|
||||||
|
| 验证通道 | `03010101` |
|
||||||
|
| 端头预览 | 1 托 |
|
||||||
|
| 预览签名 | 存在 |
|
||||||
|
| 预览合计库存 | 1 |
|
||||||
|
| 库存明细合计 | 1 |
|
||||||
|
| KPI 同层移库源通道 | 7 条 |
|
||||||
|
| KPI 同层移库目标通道 | 44 条 |
|
||||||
|
|
||||||
|
预览合计与准确选中货位的库存明细合计一致,避免重复托盘码导致跨货位重复汇总。
|
||||||
|
|
||||||
|
## 页面验证
|
||||||
|
|
||||||
|
执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:WMS_TEST_USERNAME='admin'
|
||||||
|
$env:WMS_TEST_PASSWORD='admin123'
|
||||||
|
python scripts\verify_dense_outbound_ui.py
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:退出码 0,浏览器控制台错误 0,请求失败 0。桌面视口宽度 1440px 和窄屏视口宽度 390px 的页面文档宽度均等于视口宽度,没有页面级横向溢出。
|
||||||
|
|
||||||
|
截图:
|
||||||
|
|
||||||
|
- [出库作业 WCS 状态与人工下发](screenshots/2026-07-16-dense-outbound/outbound-job-desktop.png)
|
||||||
|
- [指定通道出库列表](screenshots/2026-07-16-dense-outbound/dense-lane-outbound-desktop.png)
|
||||||
|
- [真实数据出库预览弹窗](screenshots/2026-07-16-dense-outbound/dense-lane-outbound-dialog-real.png)
|
||||||
|
- [只读模拟数据完整弹窗](screenshots/2026-07-16-dense-outbound/dense-lane-outbound-dialog.png)
|
||||||
|
- [指定通道出库窄屏](screenshots/2026-07-16-dense-outbound/dense-lane-outbound-narrow.png)
|
||||||
|
- [KPI 货位平面图与指定移库](screenshots/2026-07-16-dense-outbound/kpi-plan-desktop.png)
|
||||||
|
|
||||||
|
真实弹窗截图在预览加载遮罩消失且托盘行渲染完成后生成,能看到准确托盘、货位和库存数量。
|
||||||
|
|
||||||
|
## 六项需求映射
|
||||||
|
|
||||||
|
| 需求 | 实现与直接证据 |
|
||||||
|
| --- | --- |
|
||||||
|
| WCS 失败补偿、最多 5 次自动重试、人工下发、最外侧校验 | Outbox 状态机、Quartz 任务、统一下发服务;`OutboundDispatchServiceImplTest` 15 项通过,出库作业页面验证通过。 |
|
||||||
|
| 指定托盘被遮挡时自动生成类型 23 移库 | 出库分配调用遮挡移库编排;`OutStockoutServiceImplTest`、`RelocationJobServiceImplTest` 通过。 |
|
||||||
|
| KPI 平面图指定同层移库 | 同仓同层、不同通道及容量复核;KPI 测试通过,真实选项为源 7、目标 44,页面正常。 |
|
||||||
|
| 入库、普通出库、指定货位/通道、移库分别使用 WCS 地址 | 按 `in_or_out` 路由到 putaway、retrieve、location-outbound、relocation;`WcsClientTest` 和 dispatch 路由测试通过。 |
|
||||||
|
| 指定货位出库被遮挡时先生成移库 | `LocationOutboundServiceImpl` 接入统一遮挡编排;对应服务测试通过。 |
|
||||||
|
| 指定通道按端头 N 托整托出库 | 列表、库存明细、准确预览签名、出库口适配和生成流程已实现;真实预览 1 托且合计一致,桌面与窄屏页面验证通过。 |
|
||||||
|
|
||||||
|
## 边界说明
|
||||||
|
|
||||||
|
- 本轮没有在现有业务库提交真实出库、移库或 WCS 失败作业;状态流转、库存搬移和失败补偿由自动化测试验证。
|
||||||
|
- 真实 WCS 联调仍需在可控测试单据和 WCS 测试地址下执行,避免向生产设备下发任务。
|
||||||
|
- 前端生产构建只有现有项目的包体积警告,不影响本次功能验收。
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
@@ -173,11 +173,14 @@ public class DenseLaneOutboundServiceImpl implements IDenseLaneOutboundService
|
|||||||
position.getCargoSpaceId(), position.getTrayCode());
|
position.getCargoSpaceId(), position.getTrayCode());
|
||||||
position.setStockDetails(details == null ? new ArrayList<>() : details);
|
position.setStockDetails(details == null ? new ArrayList<>() : details);
|
||||||
});
|
});
|
||||||
String[] trayCodes = selected.stream().map(DenseLanePosition::getTrayCode).toArray(String[]::new);
|
BigDecimal total = selected.stream()
|
||||||
BigDecimal total = relocationJobMapper.sumTrayStockQuantity(trayCodes);
|
.flatMap(position -> position.getStockDetails().stream())
|
||||||
|
.map(MydoJobDetail::getStockQuantity)
|
||||||
|
.filter(quantity -> quantity != null)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
DenseLaneOutboundPreview preview = new DenseLaneOutboundPreview();
|
DenseLaneOutboundPreview preview = new DenseLaneOutboundPreview();
|
||||||
preview.setTrays(selected);
|
preview.setTrays(selected);
|
||||||
preview.setTotalQuantity(total == null ? BigDecimal.ZERO : total);
|
preview.setTotalQuantity(total);
|
||||||
preview.setPreviewToken(previewToken(request, selected));
|
preview.setPreviewToken(previewToken(request, selected));
|
||||||
return preview;
|
return preview;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.ruoyi.out.relocation.mapper;
|
package com.ruoyi.out.relocation.mapper;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import com.ruoyi.in.job.domain.MydoJob;
|
import com.ruoyi.in.job.domain.MydoJob;
|
||||||
@@ -21,8 +20,6 @@ public interface RelocationJobMapper
|
|||||||
|
|
||||||
List<DenseLanePosition> selectRelocationSourcesForUpdate(RelocationGenerateRequest request);
|
List<DenseLanePosition> selectRelocationSourcesForUpdate(RelocationGenerateRequest request);
|
||||||
|
|
||||||
BigDecimal sumTrayStockQuantity(@Param("trayCodes") String[] trayCodes);
|
|
||||||
|
|
||||||
int reserveTarget(Map<String, Object> params);
|
int reserveTarget(Map<String, Object> params);
|
||||||
|
|
||||||
int reserveSource(Map<String, Object> params);
|
int reserveSource(Map<String, Object> params);
|
||||||
|
|||||||
@@ -82,9 +82,11 @@ public class RelocationJobServiceImpl implements IRelocationJobService
|
|||||||
.stream().limit(request.getTrayCount()).collect(Collectors.toList());
|
.stream().limit(request.getTrayCount()).collect(Collectors.toList());
|
||||||
RelocationPreview preview = new RelocationPreview();
|
RelocationPreview preview = new RelocationPreview();
|
||||||
preview.setTrays(selected);
|
preview.setTrays(selected);
|
||||||
String[] trayCodes = selected.stream().map(DenseLanePosition::getTrayCode).toArray(String[]::new);
|
BigDecimal total = selected.stream()
|
||||||
BigDecimal total = mapper.sumTrayStockQuantity(trayCodes);
|
.map(DenseLanePosition::getStockQuantity)
|
||||||
preview.setTotalQuantity(total == null ? BigDecimal.ZERO : total);
|
.filter(quantity -> quantity != null)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
preview.setTotalQuantity(total);
|
||||||
return preview;
|
return preview;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,30 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<result property="maxColumn" column="max_column" />
|
<result property="maxColumn" column="max_column" />
|
||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
|
<resultMap id="DenseLaneOptionResult" type="com.ruoyi.out.dense.domain.DenseLaneOption">
|
||||||
|
<id property="groupCoding" column="group_coding" />
|
||||||
|
<result property="cargoArea" column="cargo_area" />
|
||||||
|
<result property="thePlatoon" column="the_platoon" />
|
||||||
|
<result property="theFloor" column="the_floor" />
|
||||||
|
<result property="directionType" column="direction_type" />
|
||||||
|
<result property="fullTrayCount" column="full_tray_count" />
|
||||||
|
<result property="emptyLocationCount" column="empty_location_count" />
|
||||||
|
<result property="totalQuantity" column="total_quantity" />
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<resultMap id="DenseLaneSummaryResult" type="com.ruoyi.out.dense.domain.DenseLaneSummary">
|
||||||
|
<id property="groupCoding" column="group_coding" />
|
||||||
|
<result property="cargoArea" column="cargo_area" />
|
||||||
|
<result property="thePlatoon" column="the_platoon" />
|
||||||
|
<result property="theFloor" column="the_floor" />
|
||||||
|
<result property="directionType" column="direction_type" />
|
||||||
|
<result property="fullTrayCount" column="full_tray_count" />
|
||||||
|
<result property="availableTrayCount" column="available_tray_count" />
|
||||||
|
<result property="totalQuantity" column="total_quantity" />
|
||||||
|
<result property="outermostTrayCode" column="outermost_tray_code" />
|
||||||
|
<result property="outermostCargoSpaceId" column="outermost_cargo_space_id" />
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
<select id="selectPosition" resultMap="DenseLanePositionResult">
|
<select id="selectPosition" resultMap="DenseLanePositionResult">
|
||||||
select source_space.cargo_space_id,
|
select source_space.cargo_space_id,
|
||||||
source_space.cargo_space_name,
|
source_space.cargo_space_name,
|
||||||
@@ -178,7 +202,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
|
|
||||||
<select id="selectRelocationTargetForUpdate"
|
<select id="selectRelocationTargetForUpdate"
|
||||||
parameterType="com.ruoyi.out.dense.domain.RelocationTargetQuery"
|
parameterType="com.ruoyi.out.dense.domain.RelocationTargetQuery"
|
||||||
resultType="com.ruoyi.bas.cargospace.domain.BasCargoSpace">
|
resultMap="com.ruoyi.bas.cargospace.mapper.BasCargoSpaceMapper.BasCargoSpaceResult">
|
||||||
select top 1 target_space.*
|
select top 1 target_space.*
|
||||||
from bas_cargo_space target_space with (UPDLOCK,HOLDLOCK,ROWLOCK)
|
from bas_cargo_space target_space with (UPDLOCK,HOLDLOCK,ROWLOCK)
|
||||||
left join bas_cargo_area_config area_config
|
left join bas_cargo_area_config area_config
|
||||||
@@ -235,7 +259,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
target_space.cargo_space_id asc
|
target_space.cargo_space_id asc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectLaneOptions" resultType="com.ruoyi.out.dense.domain.DenseLaneOption">
|
<select id="selectLaneOptions" resultMap="DenseLaneOptionResult">
|
||||||
;with reserved_targets as
|
;with reserved_targets as
|
||||||
(
|
(
|
||||||
select distinct target_cargo_space_id as cargo_space_id
|
select distinct target_cargo_space_id as cargo_space_id
|
||||||
@@ -282,7 +306,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
order by lane.group_coding
|
order by lane.group_coding
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectLaneSummaries" resultType="com.ruoyi.out.dense.domain.DenseLaneSummary">
|
<select id="selectLaneSummaries" resultMap="DenseLaneSummaryResult">
|
||||||
;with lane_positions as
|
;with lane_positions as
|
||||||
(
|
(
|
||||||
select cargo.cargo_space_id,
|
select cargo.cargo_space_id,
|
||||||
|
|||||||
@@ -4,14 +4,49 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="com.ruoyi.out.relocation.mapper.RelocationJobMapper">
|
<mapper namespace="com.ruoyi.out.relocation.mapper.RelocationJobMapper">
|
||||||
|
|
||||||
<select id="selectJobForUpdate" resultType="com.ruoyi.in.job.domain.MydoJob">
|
<resultMap id="DenseLanePositionResult" type="com.ruoyi.out.dense.domain.DenseLanePosition">
|
||||||
|
<id property="cargoSpaceId" column="cargo_space_id" />
|
||||||
|
<result property="cargoSpaceName" column="cargo_space_name" />
|
||||||
|
<result property="warehouseId" column="warehouse_id" />
|
||||||
|
<result property="groupCoding" column="group_coding" />
|
||||||
|
<result property="cargoArea" column="cargo_area" />
|
||||||
|
<result property="thePlatoon" column="the_platoon" />
|
||||||
|
<result property="theColumn" column="the_column" />
|
||||||
|
<result property="theFloor" column="the_floor" />
|
||||||
|
<result property="status" column="status" />
|
||||||
|
<result property="locked" column="locked" />
|
||||||
|
<result property="trayCode" column="tray_code" />
|
||||||
|
<result property="directionType" column="direction_type" />
|
||||||
|
<result property="minColumn" column="min_column" />
|
||||||
|
<result property="maxColumn" column="max_column" />
|
||||||
|
<result property="stockQuantity" column="stock_quantity" />
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<resultMap id="StockDetailResult" type="com.ruoyi.in.jobdetail.domain.MydoJobDetail">
|
||||||
|
<result property="stockId" column="stock_id" />
|
||||||
|
<result property="lotNo" column="lot_no" />
|
||||||
|
<result property="inDate" column="in_date" />
|
||||||
|
<result property="stockinId" column="stockin_id" />
|
||||||
|
<result property="stockinDetailId" column="stockin_detail_id" />
|
||||||
|
<result property="productId" column="product_id" />
|
||||||
|
<result property="productName" column="product_name" />
|
||||||
|
<result property="stockQuantity" column="stock_quantity" />
|
||||||
|
<result property="customerId" column="customer_id" />
|
||||||
|
<result property="customerName" column="customer_name" />
|
||||||
|
<result property="supplierId" column="supplier_id" />
|
||||||
|
<result property="supplierName" column="supplier_name" />
|
||||||
|
<result property="projectNo" column="project_no" />
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<select id="selectJobForUpdate"
|
||||||
|
resultMap="com.ruoyi.in.job.mapper.MydoJobMapper.MydoJobResult">
|
||||||
select *
|
select *
|
||||||
from mydo_job with (UPDLOCK,HOLDLOCK)
|
from mydo_job with (UPDLOCK,HOLDLOCK)
|
||||||
where job_id = #{jobId}
|
where job_id = #{jobId}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectStockDetailsForUpdate"
|
<select id="selectStockDetailsForUpdate"
|
||||||
resultType="com.ruoyi.in.jobdetail.domain.MydoJobDetail">
|
resultMap="StockDetailResult">
|
||||||
select detail.stock_id,
|
select detail.stock_id,
|
||||||
detail.lot_no,
|
detail.lot_no,
|
||||||
convert(varchar(19), stock.in_date, 120) as in_date,
|
convert(varchar(19), stock.in_date, 120) as in_date,
|
||||||
@@ -22,8 +57,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
detail.quantity as stock_quantity,
|
detail.quantity as stock_quantity,
|
||||||
cast(null as nvarchar(255)) as customer_id,
|
cast(null as nvarchar(255)) as customer_id,
|
||||||
cast(null as nvarchar(255)) as customer_name,
|
cast(null as nvarchar(255)) as customer_name,
|
||||||
stock.suplier_id as supplier_id,
|
stock.supplier_id as supplier_id,
|
||||||
stock.suplier_name as supplier_name,
|
stock.supplier_name as supplier_name,
|
||||||
detail.project_no
|
detail.project_no
|
||||||
from bas_stock stock with (UPDLOCK,HOLDLOCK)
|
from bas_stock stock with (UPDLOCK,HOLDLOCK)
|
||||||
inner join bas_stock_detail detail with (UPDLOCK,HOLDLOCK)
|
inner join bas_stock_detail detail with (UPDLOCK,HOLDLOCK)
|
||||||
@@ -35,7 +70,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectRelocationSourcesForUpdate"
|
<select id="selectRelocationSourcesForUpdate"
|
||||||
resultType="com.ruoyi.out.dense.domain.DenseLanePosition">
|
resultMap="DenseLanePositionResult">
|
||||||
select cargo.cargo_space_id,
|
select cargo.cargo_space_id,
|
||||||
cargo.cargo_space_name,
|
cargo.cargo_space_name,
|
||||||
cargo.warehouse_id,
|
cargo.warehouse_id,
|
||||||
@@ -87,16 +122,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
)
|
)
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="sumTrayStockQuantity" resultType="java.math.BigDecimal">
|
|
||||||
select isnull(sum(isnull(detail.quantity, 0)), 0)
|
|
||||||
from bas_stock stock
|
|
||||||
inner join bas_stock_detail detail on detail.stock_id = stock.stock_id
|
|
||||||
where stock.tray_code in
|
|
||||||
<foreach collection="trayCodes" item="trayCode" open="(" separator="," close=")">
|
|
||||||
#{trayCode}
|
|
||||||
</foreach>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<update id="reserveTarget" parameterType="java.util.Map">
|
<update id="reserveTarget" parameterType="java.util.Map">
|
||||||
update bas_cargo_space
|
update bas_cargo_space
|
||||||
set status = '3', tray_code = #{trayCode}
|
set status = '3', tray_code = #{trayCode}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ class DenseLaneMapperSqlTest
|
|||||||
String sql = section(xml, "<select id=\"selectRelocationTargetForUpdate\"", "</select>");
|
String sql = section(xml, "<select id=\"selectRelocationTargetForUpdate\"", "</select>");
|
||||||
|
|
||||||
assertThat(sql)
|
assertThat(sql)
|
||||||
|
.contains("resultMap=\"com.ruoyi.bas.cargospace.mapper.BasCargoSpaceMapper.BasCargoSpaceResult\"")
|
||||||
.containsIgnoringCase("UPDLOCK")
|
.containsIgnoringCase("UPDLOCK")
|
||||||
.containsIgnoringCase("HOLDLOCK")
|
.containsIgnoringCase("HOLDLOCK")
|
||||||
.contains("warehouse_id = #{warehouseId}")
|
.contains("warehouse_id = #{warehouseId}")
|
||||||
@@ -82,6 +83,25 @@ class DenseLaneMapperSqlTest
|
|||||||
.doesNotContain("abs(target_space.the_column - 1)");
|
.doesNotContain("abs(target_space.the_column - 1)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void laneOptionsAndSummariesUseExplicitSnakeCaseResultMaps() throws Exception
|
||||||
|
{
|
||||||
|
String xml = Files.readString(resolveMapperXml());
|
||||||
|
String optionMap = section(xml, "<resultMap id=\"DenseLaneOptionResult\"", "</resultMap>");
|
||||||
|
String summaryMap = section(xml, "<resultMap id=\"DenseLaneSummaryResult\"", "</resultMap>");
|
||||||
|
String optionSelect = section(xml, "<select id=\"selectLaneOptions\"", "</select>");
|
||||||
|
String summarySelect = section(xml, "<select id=\"selectLaneSummaries\"", "</select>");
|
||||||
|
|
||||||
|
assertThat(optionMap)
|
||||||
|
.contains("property=\"groupCoding\" column=\"group_coding\"")
|
||||||
|
.contains("property=\"emptyLocationCount\" column=\"empty_location_count\"");
|
||||||
|
assertThat(summaryMap)
|
||||||
|
.contains("property=\"groupCoding\" column=\"group_coding\"")
|
||||||
|
.contains("property=\"outermostTrayCode\" column=\"outermost_tray_code\"");
|
||||||
|
assertThat(optionSelect).contains("resultMap=\"DenseLaneOptionResult\"");
|
||||||
|
assertThat(summarySelect).contains("resultMap=\"DenseLaneSummaryResult\"");
|
||||||
|
}
|
||||||
|
|
||||||
private Path resolveMapperXml()
|
private Path resolveMapperXml()
|
||||||
{
|
{
|
||||||
return Stream.of(
|
return Stream.of(
|
||||||
|
|||||||
@@ -106,8 +106,10 @@ class DenseLaneOutboundServiceImplTest
|
|||||||
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(positions);
|
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(positions);
|
||||||
when(denseLaneService.orderOutermostFirst(positions))
|
when(denseLaneService.orderOutermostFirst(positions))
|
||||||
.thenReturn(List.of(positions.get(1), positions.get(0), positions.get(2)));
|
.thenReturn(List.of(positions.get(1), positions.get(0), positions.get(2)));
|
||||||
when(relocationJobMapper.sumTrayStockQuantity(any(String[].class)))
|
when(relocationJobMapper.selectStockDetailsForUpdate("CS-3", "A003"))
|
||||||
.thenReturn(new BigDecimal("25"));
|
.thenReturn(List.of(detail("STOCK-3", "D-3", "P-3", "15")));
|
||||||
|
when(relocationJobMapper.selectStockDetailsForUpdate("CS-2", "A002"))
|
||||||
|
.thenReturn(List.of(detail("STOCK-2", "D-2", "P-2", "10")));
|
||||||
|
|
||||||
DenseLaneOutboundPreview preview = service.preview(request);
|
DenseLaneOutboundPreview preview = service.preview(request);
|
||||||
|
|
||||||
@@ -128,8 +130,6 @@ class DenseLaneOutboundServiceImplTest
|
|||||||
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(position));
|
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(position));
|
||||||
when(denseLaneService.orderOutermostFirst(any())).thenReturn(List.of(position));
|
when(denseLaneService.orderOutermostFirst(any())).thenReturn(List.of(position));
|
||||||
when(relocationJobMapper.selectStockDetailsForUpdate("CS-1", "A001")).thenReturn(details);
|
when(relocationJobMapper.selectStockDetailsForUpdate("CS-1", "A001")).thenReturn(details);
|
||||||
when(relocationJobMapper.sumTrayStockQuantity(any(String[].class)))
|
|
||||||
.thenReturn(new BigDecimal("12"));
|
|
||||||
|
|
||||||
DenseLaneOutboundPreview preview = service.preview(request);
|
DenseLaneOutboundPreview preview = service.preview(request);
|
||||||
|
|
||||||
@@ -137,6 +137,22 @@ class DenseLaneOutboundServiceImplTest
|
|||||||
assertThat(tray.getStockDetails()).containsExactlyElementsOf(details));
|
assertThat(tray.getStockDetails()).containsExactlyElementsOf(details));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void previewTotalUsesOnlySelectedCargoSpaceInventoryDetails()
|
||||||
|
{
|
||||||
|
DenseLaneOutboundRequest request = request(1);
|
||||||
|
DenseLanePosition position = position("CS-1", 1L, "DUPLICATE-TRAY");
|
||||||
|
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(position));
|
||||||
|
when(denseLaneService.orderOutermostFirst(any())).thenReturn(List.of(position));
|
||||||
|
when(relocationJobMapper.selectStockDetailsForUpdate("CS-1", "DUPLICATE-TRAY"))
|
||||||
|
.thenReturn(List.of(detail("STOCK-1", "D-1", "P-1", "7"),
|
||||||
|
detail("STOCK-1", "D-2", "P-2", "5")));
|
||||||
|
|
||||||
|
DenseLaneOutboundPreview preview = service.preview(request);
|
||||||
|
|
||||||
|
assertThat(preview.getTotalQuantity()).isEqualByComparingTo("12");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void previewRejectsQuantityOverAvailableCapacity()
|
void previewRejectsQuantityOverAvailableCapacity()
|
||||||
{
|
{
|
||||||
@@ -154,7 +170,6 @@ class DenseLaneOutboundServiceImplTest
|
|||||||
DenseLanePosition position = position("CS-1", 1L, "A001");
|
DenseLanePosition position = position("CS-1", 1L, "A001");
|
||||||
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(position));
|
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(position));
|
||||||
when(denseLaneService.orderOutermostFirst(any())).thenReturn(List.of(position));
|
when(denseLaneService.orderOutermostFirst(any())).thenReturn(List.of(position));
|
||||||
when(relocationJobMapper.sumTrayStockQuantity(any(String[].class))).thenReturn(BigDecimal.TEN);
|
|
||||||
when(relocationJobMapper.selectStockDetailsForUpdate("CS-1", "A001"))
|
when(relocationJobMapper.selectStockDetailsForUpdate("CS-1", "A001"))
|
||||||
.thenReturn(List.of(detail("STOCK-1", "D-1", "P-1", "10")))
|
.thenReturn(List.of(detail("STOCK-1", "D-1", "P-1", "10")))
|
||||||
.thenReturn(List.of(detail("STOCK-1", "D-1", "P-1", "9")));
|
.thenReturn(List.of(detail("STOCK-1", "D-1", "P-1", "9")));
|
||||||
@@ -172,7 +187,6 @@ class DenseLaneOutboundServiceImplTest
|
|||||||
DenseLanePosition first = position("CS-1", 1L, "A001");
|
DenseLanePosition first = position("CS-1", 1L, "A001");
|
||||||
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(first));
|
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(first));
|
||||||
when(denseLaneService.orderOutermostFirst(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
when(denseLaneService.orderOutermostFirst(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
when(relocationJobMapper.sumTrayStockQuantity(any(String[].class))).thenReturn(BigDecimal.TEN);
|
|
||||||
request.setPreviewToken(service.preview(request).getPreviewToken());
|
request.setPreviewToken(service.preview(request).getPreviewToken());
|
||||||
DenseLanePosition changed = position("CS-2", 2L, "A002");
|
DenseLanePosition changed = position("CS-2", 2L, "A002");
|
||||||
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(changed));
|
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(changed));
|
||||||
@@ -187,7 +201,6 @@ class DenseLaneOutboundServiceImplTest
|
|||||||
DenseLanePosition position = position("CS-1", 1L, "A001");
|
DenseLanePosition position = position("CS-1", 1L, "A001");
|
||||||
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(position));
|
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(position));
|
||||||
when(denseLaneService.orderOutermostFirst(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
when(denseLaneService.orderOutermostFirst(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
when(relocationJobMapper.sumTrayStockQuantity(any(String[].class))).thenReturn(new BigDecimal("12"));
|
|
||||||
List<MydoJobDetail> stockDetails = List.of(detail("STOCK-1", "D-1", "P-1", "7"),
|
List<MydoJobDetail> stockDetails = List.of(detail("STOCK-1", "D-1", "P-1", "7"),
|
||||||
detail("STOCK-1", "D-2", "P-2", "5"));
|
detail("STOCK-1", "D-2", "P-2", "5"));
|
||||||
when(relocationJobMapper.selectStockDetailsForUpdate("CS-1", "A001"))
|
when(relocationJobMapper.selectStockDetailsForUpdate("CS-1", "A001"))
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.ruoyi.out.relocation.mapper;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class RelocationJobMapperSqlTest
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
void relocationRuntimeQueriesUseExplicitSnakeCaseResultMaps() throws Exception
|
||||||
|
{
|
||||||
|
String xml = Files.readString(resolveMapperXml());
|
||||||
|
String positionMap = section(xml, "<resultMap id=\"DenseLanePositionResult\"", "</resultMap>");
|
||||||
|
String stockMap = section(xml, "<resultMap id=\"StockDetailResult\"", "</resultMap>");
|
||||||
|
String jobSelect = section(xml, "<select id=\"selectJobForUpdate\"", "</select>");
|
||||||
|
String stockSelect = section(xml, "<select id=\"selectStockDetailsForUpdate\"", "</select>");
|
||||||
|
String sourceSelect = section(xml, "<select id=\"selectRelocationSourcesForUpdate\"", "</select>");
|
||||||
|
|
||||||
|
assertThat(positionMap)
|
||||||
|
.contains("property=\"cargoSpaceId\" column=\"cargo_space_id\"")
|
||||||
|
.contains("property=\"stockQuantity\" column=\"stock_quantity\"");
|
||||||
|
assertThat(stockMap)
|
||||||
|
.contains("property=\"stockId\" column=\"stock_id\"")
|
||||||
|
.contains("property=\"stockinDetailId\" column=\"stockin_detail_id\"")
|
||||||
|
.contains("property=\"projectNo\" column=\"project_no\"");
|
||||||
|
assertThat(jobSelect)
|
||||||
|
.contains("resultMap=\"com.ruoyi.in.job.mapper.MydoJobMapper.MydoJobResult\"");
|
||||||
|
assertThat(stockSelect).contains("resultMap=\"StockDetailResult\"");
|
||||||
|
assertThat(sourceSelect).contains("resultMap=\"DenseLanePositionResult\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stockDetailQueryUsesActualBasStockSupplierColumns() throws Exception
|
||||||
|
{
|
||||||
|
String xml = Files.readString(resolveMapperXml());
|
||||||
|
String stockSelect = section(xml, "<select id=\"selectStockDetailsForUpdate\"", "</select>");
|
||||||
|
|
||||||
|
assertThat(stockSelect)
|
||||||
|
.contains("stock.supplier_id as supplier_id")
|
||||||
|
.contains("stock.supplier_name as supplier_name")
|
||||||
|
.doesNotContain("stock.suplier_id")
|
||||||
|
.doesNotContain("stock.suplier_name");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path resolveMapperXml()
|
||||||
|
{
|
||||||
|
return Stream.of(
|
||||||
|
Path.of("src/main/resources/mapper/out/RelocationJobMapper.xml"),
|
||||||
|
Path.of("ruoyi-admin/src/main/resources/mapper/out/RelocationJobMapper.xml"))
|
||||||
|
.filter(Files::exists)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String section(String text, String startMarker, String endMarker)
|
||||||
|
{
|
||||||
|
int start = text.indexOf(startMarker);
|
||||||
|
assertThat(start).isGreaterThanOrEqualTo(0);
|
||||||
|
int end = text.indexOf(endMarker, start);
|
||||||
|
assertThat(end).isGreaterThan(start);
|
||||||
|
return text.substring(start, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -118,6 +118,24 @@ class RelocationJobServiceImplTest
|
|||||||
.hasMessageContaining("不同通道");
|
.hasMessageContaining("不同通道");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void previewTotalUsesExactSelectedCargoSpaceQuantities()
|
||||||
|
{
|
||||||
|
RelocationGenerateRequest request = new RelocationGenerateRequest();
|
||||||
|
request.setWarehouseId("03");
|
||||||
|
request.setTheFloor(1L);
|
||||||
|
request.setSourceGroupCoding("SOURCE-GROUP");
|
||||||
|
request.setTargetGroupCoding("TARGET-GROUP");
|
||||||
|
request.setTrayCount(2);
|
||||||
|
DenseLanePosition first = source("CS-1", "DUPLICATE-TRAY");
|
||||||
|
first.setStockQuantity(new BigDecimal("7"));
|
||||||
|
DenseLanePosition second = source("CS-2", "UNIQUE-TRAY");
|
||||||
|
second.setStockQuantity(new BigDecimal("5"));
|
||||||
|
when(mapper.selectRelocationSourcesForUpdate(request)).thenReturn(List.of(first, second));
|
||||||
|
when(denseLaneService.orderOutermostFirst(any())).thenReturn(List.of(first, second));
|
||||||
|
assertThat(service.preview(request).getTotalQuantity()).isEqualByComparingTo("12");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void completionMovesInventorySwapsCargoSpacesArchivesAndWakesNextJob()
|
void completionMovesInventorySwapsCargoSpacesArchivesAndWakesNextJob()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ import TrayCargoSpaceMap from './components/TrayCargoSpaceMap.vue'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
// 必须与菜单路由名称一致,keep-alive 才能缓存,避免切页/聚焦后整页重载
|
// 必须与菜单路由名称一致,keep-alive 才能缓存,避免切页/聚焦后整页重载
|
||||||
name: '货位平面图',
|
name: 'PlanKpi',
|
||||||
components: {
|
components: {
|
||||||
DenseCargoSpaceMap,
|
DenseCargoSpaceMap,
|
||||||
TrayCargoSpaceMap
|
TrayCargoSpaceMap
|
||||||
|
|||||||
168
scripts/verify_dense_outbound_ui.py
Normal file
168
scripts/verify_dense_outbound_ui.py
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
"""Read-only Playwright verification for dense outbound and relocation screens."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
|
||||||
|
BASE_URL = os.getenv("WMS_TEST_BASE_URL", "http://127.0.0.1")
|
||||||
|
USERNAME = os.environ["WMS_TEST_USERNAME"]
|
||||||
|
PASSWORD = os.environ["WMS_TEST_PASSWORD"]
|
||||||
|
OUTPUT_DIR = Path(os.getenv(
|
||||||
|
"WMS_TEST_SCREENSHOT_DIR",
|
||||||
|
"docs/testing/screenshots/2026-07-16-dense-outbound",
|
||||||
|
)).resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_app(page):
|
||||||
|
try:
|
||||||
|
page.wait_for_load_state("networkidle", timeout=15000)
|
||||||
|
except PlaywrightTimeoutError:
|
||||||
|
page.wait_for_load_state("domcontentloaded")
|
||||||
|
page.wait_for_timeout(800)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_text(page, *labels):
|
||||||
|
body = page.locator("body").inner_text()
|
||||||
|
for label in labels:
|
||||||
|
if label not in body:
|
||||||
|
raise AssertionError(f"Missing text {label!r} at {page.url}")
|
||||||
|
|
||||||
|
|
||||||
|
def page_overflow(page):
|
||||||
|
return page.evaluate("""() => ({
|
||||||
|
viewport: window.innerWidth,
|
||||||
|
documentWidth: document.documentElement.scrollWidth,
|
||||||
|
bodyWidth: document.body.scrollWidth
|
||||||
|
})""")
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_preview_dialog(page):
|
||||||
|
dialog = page.locator(".el-dialog:visible")
|
||||||
|
dialog.get_by_text("指定通道整托出库", exact=True).wait_for(timeout=10000)
|
||||||
|
dialog.locator(".el-loading-mask").wait_for(state="hidden", timeout=10000)
|
||||||
|
dialog.locator("tbody .el-table__row").first.wait_for(timeout=10000)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
results = {"pages": {}, "consoleErrors": [], "requestFailures": []}
|
||||||
|
with sync_playwright() as playwright:
|
||||||
|
browser = playwright.chromium.launch(channel="chrome", headless=True)
|
||||||
|
context = browser.new_context(viewport={"width": 1440, "height": 1000})
|
||||||
|
page = context.new_page()
|
||||||
|
page.on("console", lambda msg: results["consoleErrors"].append(f"{page.url} :: {msg.text}")
|
||||||
|
if msg.type == "error" else None)
|
||||||
|
page.on("requestfailed", lambda request: results["requestFailures"].append(
|
||||||
|
f"{request.method} {request.url}: {request.failure}"))
|
||||||
|
|
||||||
|
page.goto(BASE_URL + "/login", wait_until="domcontentloaded")
|
||||||
|
wait_for_app(page)
|
||||||
|
page.get_by_placeholder("账号").fill(USERNAME)
|
||||||
|
page.get_by_placeholder("密码").fill(PASSWORD)
|
||||||
|
page.get_by_role("button", name="登 录").click()
|
||||||
|
page.wait_for_url(lambda url: "/login" not in url, timeout=15000)
|
||||||
|
wait_for_app(page)
|
||||||
|
|
||||||
|
pages = [
|
||||||
|
("outbound-job", "/out/outJob",
|
||||||
|
("WCS状态", "自动重试", "最后下发错误", "手动下发 WCS")),
|
||||||
|
("dense-lane-outbound", "/out/denseLaneOutbound",
|
||||||
|
("通道组编码", "满托盘数", "可出库托盘", "当前最外侧托盘", "整托出库")),
|
||||||
|
("kpi-plan", "/kpi/planKpi",
|
||||||
|
("货位平面图", "指定移库")),
|
||||||
|
]
|
||||||
|
for name, route, labels in pages:
|
||||||
|
page.goto(BASE_URL + route, wait_until="domcontentloaded")
|
||||||
|
wait_for_app(page)
|
||||||
|
assert_text(page, *labels)
|
||||||
|
screenshot = OUTPUT_DIR / f"{name}-desktop.png"
|
||||||
|
page.screenshot(path=str(screenshot), full_page=True)
|
||||||
|
results["pages"][name] = {
|
||||||
|
"url": page.url,
|
||||||
|
"desktopScreenshot": str(screenshot),
|
||||||
|
"desktopOverflow": page_overflow(page),
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "dense-lane-outbound":
|
||||||
|
lane_buttons = page.get_by_role("button", name="整托出库")
|
||||||
|
enabled_button = next((lane_buttons.nth(index)
|
||||||
|
for index in range(lane_buttons.count())
|
||||||
|
if lane_buttons.nth(index).is_enabled()), None)
|
||||||
|
if enabled_button is not None:
|
||||||
|
enabled_button.click()
|
||||||
|
wait_for_preview_dialog(page)
|
||||||
|
assert_text(page, "整托数量", "出库口", "本次准确出库托盘")
|
||||||
|
dialog_shot = OUTPUT_DIR / "dense-lane-outbound-dialog-real.png"
|
||||||
|
page.screenshot(path=str(dialog_shot), full_page=True)
|
||||||
|
results["pages"][name]["realDialogScreenshot"] = str(dialog_shot)
|
||||||
|
page.get_by_role("button", name="取消").last.click()
|
||||||
|
else:
|
||||||
|
results["pages"][name]["dialog"] = "skipped-no-available-lane"
|
||||||
|
|
||||||
|
synthetic_lane = {
|
||||||
|
"groupCoding": "VERIFY-LANE-01", "cargoArea": "G", "thePlatoon": 7,
|
||||||
|
"theFloor": 1, "directionType": "1", "fullTrayCount": 3,
|
||||||
|
"availableTrayCount": 2, "totalQuantity": 24,
|
||||||
|
"outermostTrayCode": "A-VERIFY-001", "outermostCargoSpaceId": "VERIFY-CS-003",
|
||||||
|
}
|
||||||
|
synthetic_detail = {
|
||||||
|
"projectNo": "VERIFY-PROJECT", "productId": "VERIFY-MATERIAL",
|
||||||
|
"productName": "验证物料", "lotNo": "VERIFY-LOT", "stockQuantity": 12,
|
||||||
|
}
|
||||||
|
synthetic_tray = {
|
||||||
|
"cargoSpaceId": "VERIFY-CS-003", "cargoSpaceName": "验证端头货位",
|
||||||
|
"trayCode": "A-VERIFY-001", "theColumn": 3, "stockQuantity": 12,
|
||||||
|
"stockDetails": [synthetic_detail],
|
||||||
|
}
|
||||||
|
page.route("**/dev-api/out/denseLaneOutbound/list*", lambda route: route.fulfill(
|
||||||
|
status=200, content_type="application/json",
|
||||||
|
body=json.dumps({"code": 200, "data": [synthetic_lane]}, ensure_ascii=False)))
|
||||||
|
page.route("**/dev-api/out/denseLaneOutbound/preview*", lambda route: route.fulfill(
|
||||||
|
status=200, content_type="application/json",
|
||||||
|
body=json.dumps({"code": 200, "data": {"trays": [synthetic_tray],
|
||||||
|
"totalQuantity": 12, "previewToken": "verify-token"}}, ensure_ascii=False)))
|
||||||
|
page.route("**/dev-api/bas/stationpoint/list*", lambda route: route.fulfill(
|
||||||
|
status=200, content_type="application/json",
|
||||||
|
body=json.dumps({"code": 200, "rows": [{"pointId": "VERIFY-OUT-1",
|
||||||
|
"pointName": "验证出库口", "pointType": "2", "pointStatus": "0",
|
||||||
|
"status": "0", "trayType": "A"}], "total": 1}, ensure_ascii=False)))
|
||||||
|
page.set_viewport_size({"width": 1440, "height": 1000})
|
||||||
|
page.goto(BASE_URL + "/out/denseLaneOutbound", wait_until="domcontentloaded")
|
||||||
|
wait_for_app(page)
|
||||||
|
page.get_by_role("button", name="整托出库").click()
|
||||||
|
wait_for_preview_dialog(page)
|
||||||
|
page.get_by_placeholder("请选择与托盘类型匹配的出库口").click()
|
||||||
|
page.get_by_text("验证出库口(VERIFY-OUT-1,出库,A类托盘)", exact=True).wait_for(timeout=5000)
|
||||||
|
assert_text(page, "VERIFY-LANE-01", "A-VERIFY-001", "VERIFY-CS-003",
|
||||||
|
"验证出库口", "本次准确出库托盘")
|
||||||
|
dialog_shot = OUTPUT_DIR / "dense-lane-outbound-dialog.png"
|
||||||
|
page.screenshot(path=str(dialog_shot), full_page=True)
|
||||||
|
results["pages"]["dense-lane-outbound"]["mockDialogScreenshot"] = str(dialog_shot)
|
||||||
|
results["pages"]["dense-lane-outbound"]["mockDialog"] = "verified-with-read-only-mocks"
|
||||||
|
|
||||||
|
page.set_viewport_size({"width": 390, "height": 844})
|
||||||
|
page.goto(BASE_URL + "/out/denseLaneOutbound", wait_until="domcontentloaded")
|
||||||
|
wait_for_app(page)
|
||||||
|
assert_text(page, "指定通道", "通道组编码")
|
||||||
|
narrow_shot = OUTPUT_DIR / "dense-lane-outbound-narrow.png"
|
||||||
|
page.screenshot(path=str(narrow_shot), full_page=True)
|
||||||
|
results["pages"]["dense-lane-outbound"]["narrowScreenshot"] = str(narrow_shot)
|
||||||
|
results["pages"]["dense-lane-outbound"]["narrowOverflow"] = page_overflow(page)
|
||||||
|
|
||||||
|
context.close()
|
||||||
|
browser.close()
|
||||||
|
|
||||||
|
ignored_console_fragments = ("ResizeObserver loop",)
|
||||||
|
results["consoleErrors"] = [message for message in results["consoleErrors"]
|
||||||
|
if not any(fragment in message for fragment in ignored_console_fragments)]
|
||||||
|
if results["consoleErrors"] or results["requestFailures"]:
|
||||||
|
raise AssertionError(json.dumps(results, ensure_ascii=False, indent=2))
|
||||||
|
print(json.dumps(results, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -255,6 +255,13 @@ END
|
|||||||
IF @menuIdIsIdentity = 1
|
IF @menuIdIsIdentity = 1
|
||||||
SET IDENTITY_INSERT dbo.sys_menu OFF;
|
SET IDENTITY_INSERT dbo.sys_menu OFF;
|
||||||
|
|
||||||
|
UPDATE dbo.sys_menu
|
||||||
|
SET route_name = N'PlanKpi',
|
||||||
|
update_by = N'admin',
|
||||||
|
update_time = GETDATE()
|
||||||
|
WHERE component = N'kpi/plan/index'
|
||||||
|
AND ISNULL(route_name, N'') != N'PlanKpi';
|
||||||
|
|
||||||
IF OBJECT_ID(N'dbo.sys_role_menu', N'U') IS NOT NULL
|
IF OBJECT_ID(N'dbo.sys_role_menu', N'U') IS NOT NULL
|
||||||
BEGIN
|
BEGIN
|
||||||
INSERT INTO dbo.sys_role_menu (role_id, menu_id)
|
INSERT INTO dbo.sys_role_menu (role_id, menu_id)
|
||||||
|
|||||||
Reference in New Issue
Block a user