feat: 完善WCS调度、库存追溯和出库分配
This commit is contained in:
60
scripts/verify-goal-fixes.js
Normal file
60
scripts/verify-goal-fixes.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const root = path.resolve(__dirname, '..')
|
||||
|
||||
function read(file) {
|
||||
return fs.readFileSync(path.join(root, file), 'utf8')
|
||||
}
|
||||
|
||||
function assertMatch(message, content, pattern) {
|
||||
if (!pattern.test(content)) {
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of ['ruoyi-ui/src/views/in/add/index.vue', 'ruoyi-ui/src/views/out/create/index.vue']) {
|
||||
const content = read(file)
|
||||
assertMatch(`${file} 新建单据来源必须默认自建单`, content, /stockSource:\s*'1'/)
|
||||
assertMatch(`${file} 表单栅格必须使用稳定类名`, content, /class="stable-form-grid"/)
|
||||
assertMatch(`${file} 表单控件必须固定为满宽`, content, /\.create-(stockin|stockout)[\s\S]*\.el-select[\s\S]*width:\s*100%/)
|
||||
assertMatch(`${file} 表单项必须预留错误提示高度防止选择时抖动`, content, /\.create-(stockin|stockout)[\s\S]*\.el-form-item[\s\S]*margin-bottom:\s*22px/)
|
||||
}
|
||||
|
||||
const pdaMapper = read('ruoyi-admin/src/main/resources/mapper/pda/PdaMapper.xml')
|
||||
assertMatch(
|
||||
'PDA 必须先回滚同托盘同仓库残留待绑数量',
|
||||
pdaMapper,
|
||||
/id="rollbackPendingTrayBindings"[\s\S]*scan_num[\s\S]*in_tray_msg/
|
||||
)
|
||||
assertMatch(
|
||||
'PDA 必须删除同托盘同仓库残留待绑记录',
|
||||
pdaMapper,
|
||||
/id="deletePendingTrayBindings"[\s\S]*delete\s+from\s+in_tray_msg/
|
||||
)
|
||||
|
||||
const pdService = read('ruoyi-admin/src/main/java/com/ruoyi/pd/stockpd/service/impl/PdStockpdServiceImpl.java')
|
||||
assertMatch(
|
||||
'盘点状态过程返回值必须用 Convert.toStr 兼容 INTEGER OUT 参数',
|
||||
pdService,
|
||||
/Convert\.toStr\(map\.get\("v_result"\)\)/
|
||||
)
|
||||
|
||||
const inventorySql = read('sql/20260626_inventory_status_proc.sql')
|
||||
assertMatch(
|
||||
'盘点状态过程脚本必须创建 proc_update_pdbill_status',
|
||||
inventorySql,
|
||||
/CREATE\s+PROCEDURE\s+dbo\.proc_update_pdbill_status/i
|
||||
)
|
||||
assertMatch(
|
||||
'盘点完成必须归档主单',
|
||||
inventorySql,
|
||||
/INSERT\s+INTO\s+dbo\.pd_stockpd_finish/i
|
||||
)
|
||||
assertMatch(
|
||||
'盘点完成必须归档明细',
|
||||
inventorySql,
|
||||
/INSERT\s+INTO\s+dbo\.pd_stockpd_detail_finish/i
|
||||
)
|
||||
|
||||
console.log('Goal fix checks passed')
|
||||
102
scripts/verify-inventory-cancel-and-injob.js
Normal file
102
scripts/verify-inventory-cancel-and-injob.js
Normal file
@@ -0,0 +1,102 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const root = path.resolve(__dirname, '..')
|
||||
|
||||
function read(file) {
|
||||
return fs.readFileSync(path.join(root, file), 'utf8')
|
||||
}
|
||||
|
||||
function assertMatch(message, content, pattern) {
|
||||
if (!pattern.test(content)) {
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
|
||||
function assertNotMatch(message, content, pattern) {
|
||||
if (pattern.test(content)) {
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
|
||||
const inventoryProc = read('sql/20260626_inventory_status_proc.sql')
|
||||
assertMatch(
|
||||
'盘点作废必须把主单插入历史表',
|
||||
inventoryProc,
|
||||
/IF\s+@target_status\s*=\s*N'5'[\s\S]*INSERT\s+INTO\s+dbo\.pd_stockpd_finish/i
|
||||
)
|
||||
assertMatch(
|
||||
'盘点作废必须把明细插入历史表',
|
||||
inventoryProc,
|
||||
/IF\s+@target_status\s*=\s*N'5'[\s\S]*INSERT\s+INTO\s+dbo\.pd_stockpd_detail_finish/i
|
||||
)
|
||||
assertMatch(
|
||||
'盘点作废必须删除业务明细',
|
||||
inventoryProc,
|
||||
/IF\s+@target_status\s*=\s*N'5'[\s\S]*DELETE\s+FROM\s+dbo\.pd_stockpd_detail/i
|
||||
)
|
||||
assertMatch(
|
||||
'盘点作废必须删除业务主单',
|
||||
inventoryProc,
|
||||
/IF\s+@target_status\s*=\s*N'5'[\s\S]*DELETE\s+FROM\s+dbo\.pd_stockpd/i
|
||||
)
|
||||
assertNotMatch(
|
||||
'盘点作废不能只更新业务表状态',
|
||||
inventoryProc,
|
||||
/IF\s+@target_status\s*=\s*N'5'[\s\S]*UPDATE\s+dbo\.pd_stockpd[\s\S]*SET\s+status\s*=\s*N'5'/i
|
||||
)
|
||||
|
||||
const pdInController = read('ruoyi-admin/src/main/java/com/ruoyi/pd/job/controller/PdOutMydoJobController.java')
|
||||
assertMatch(
|
||||
'/pd/inJob 当前控制器必须查询盘点入库作业列表',
|
||||
pdInController,
|
||||
/@RequestMapping\("\/pd\/inJob"\)[\s\S]*selectInventoryInMydoJobList\(mydoJob\)/
|
||||
)
|
||||
assertMatch(
|
||||
'/pd/inJob 当前控制器必须查询盘点入库历史作业列表',
|
||||
pdInController,
|
||||
/@RequestMapping\("\/pd\/inJob"\)[\s\S]*selectInventoryInMydoJobFinishList\(mydoJob\)/
|
||||
)
|
||||
assertNotMatch(
|
||||
'/pd/inJob 当前控制器不能使用出库列表口径',
|
||||
pdInController,
|
||||
/@RequestMapping\("\/pd\/inJob"\)[\s\S]*selectOutMydoJobList\(mydoJob\)/
|
||||
)
|
||||
assertMatch(
|
||||
'/pd/inJob 手动作废必须走入库作业过程',
|
||||
pdInController,
|
||||
/cancelJob\(jobId,\s*"1"\)/
|
||||
)
|
||||
assertMatch(
|
||||
'/pd/inJob 手动完成必须走入库作业过程',
|
||||
pdInController,
|
||||
/completeJob\(jobId,\s*"1"\)/
|
||||
)
|
||||
|
||||
const pdOutController = read('ruoyi-admin/src/main/java/com/ruoyi/pd/job/controller/PdInMydoJobController.java')
|
||||
assertMatch(
|
||||
'/pd/outJob 当前控制器必须查询盘点出库作业列表',
|
||||
pdOutController,
|
||||
/@RequestMapping\("\/pd\/outJob"\)[\s\S]*selectInventoryOutMydoJobList\(mydoJob\)/
|
||||
)
|
||||
assertMatch(
|
||||
'/pd/outJob 当前控制器必须查询盘点出库历史作业列表',
|
||||
pdOutController,
|
||||
/@RequestMapping\("\/pd\/outJob"\)[\s\S]*selectInventoryOutMydoJobFinishList\(mydoJob\)/
|
||||
)
|
||||
|
||||
const mydoJobMapper = read('ruoyi-admin/src/main/resources/mapper/in/MydoJobMapper.xml')
|
||||
assertMatch('盘点入库作业列表必须限定 in_or_out in (10, 11)', mydoJobMapper, /id="selectInventoryInMydoJobList"[\s\S]*in_or_out\s+in\s+\(10,\s*11\)/i)
|
||||
assertMatch('盘点入库历史作业列表必须限定 in_or_out in (10, 11)', mydoJobMapper, /id="selectInventoryInMydoJobFinishList"[\s\S]*in_or_out\s+in\s+\(10,\s*11\)/i)
|
||||
assertMatch('盘点出库作业列表必须限定 in_or_out in (30, 31)', mydoJobMapper, /id="selectInventoryOutMydoJobList"[\s\S]*in_or_out\s+in\s+\(30,\s*31\)/i)
|
||||
assertMatch('盘点出库历史作业列表必须限定 in_or_out in (30, 31)', mydoJobMapper, /id="selectInventoryOutMydoJobFinishList"[\s\S]*in_or_out\s+in\s+\(30,\s*31\)/i)
|
||||
|
||||
const pdInPage = read('ruoyi-ui/src/views/pd/job/in/index.vue')
|
||||
assertMatch('盘点入库页面必须加载盘点入库任务类型字典', pdInPage, /dicts:\s*\[[^\]]*'pd_in_job_type'/)
|
||||
assertMatch('盘点入库页面任务类型筛选必须使用盘点入库字典', pdInPage, /dict\.type\.pd_in_job_type/)
|
||||
|
||||
const pdOutPage = read('ruoyi-ui/src/views/pd/job/out/index.vue')
|
||||
assertMatch('盘点出库页面必须加载盘点出库任务类型字典', pdOutPage, /dicts:\s*\[[^\]]*'pd_out_job_type'/)
|
||||
assertMatch('盘点出库页面任务类型筛选必须使用盘点出库字典', pdOutPage, /dict\.type\.pd_out_job_type/)
|
||||
|
||||
console.log('Inventory cancel and inbound job checks passed')
|
||||
61
scripts/verify-wcs-inventory-outbound-fixes.js
Normal file
61
scripts/verify-wcs-inventory-outbound-fixes.js
Normal file
@@ -0,0 +1,61 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const root = path.resolve(__dirname, '..')
|
||||
|
||||
function read(file) {
|
||||
return fs.readFileSync(path.join(root, file), 'utf8')
|
||||
}
|
||||
|
||||
function assertMatch(message, content, pattern) {
|
||||
if (!pattern.test(content)) {
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
|
||||
function assertNotMatch(message, content, pattern) {
|
||||
if (pattern.test(content)) {
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
|
||||
function section(content, startMarker, endMarker) {
|
||||
const start = content.indexOf(startMarker)
|
||||
if (start < 0) {
|
||||
throw new Error(`未找到开始标记: ${startMarker}`)
|
||||
}
|
||||
const end = content.indexOf(endMarker, start)
|
||||
if (end < 0) {
|
||||
throw new Error(`未找到结束标记: ${endMarker}`)
|
||||
}
|
||||
return content.slice(start, end)
|
||||
}
|
||||
|
||||
const stockView = read('ruoyi-ui/src/views/bas/stock/index.vue')
|
||||
const detailDialog = section(stockView, '<!-- 添加或修改库存物料明细对话框 -->', '</el-dialog>')
|
||||
|
||||
assertMatch('库存子表必须显示项目号', stockView, /<el-table-column[^>]*label="项目号"[^>]*prop="projectNo"/)
|
||||
assertMatch('子作业 ID 输入框必须只读', detailDialog, /v-model="detailForm\.jobDetailId"[^>]*readonly/)
|
||||
assertMatch('入库子单 ID 输入框必须只读', detailDialog, /v-model="detailForm\.stockinDetailId"[^>]*readonly/)
|
||||
assertMatch('库存明细物料状态必须使用开关', detailDialog, /<el-switch[\s\S]*v-model="detailForm\.productStatus"/)
|
||||
assertMatch('物料状态开关正常值必须为字符串 1', detailDialog, /active-value="1"/)
|
||||
assertMatch('物料状态开关异常值必须为字符串 2', detailDialog, /inactive-value="2"/)
|
||||
assertMatch('新增库存明细物料状态必须默认正常', stockView, /resetDetail\(\)[\s\S]*productStatus:\s*'1'/)
|
||||
|
||||
const stockDetailMapper = read('ruoyi-admin/src/main/resources/mapper/bas/BasStockDetailMapper.xml')
|
||||
const updateDetail = section(stockDetailMapper, '<update id="updateBasStockDetail"', '</update>')
|
||||
assertNotMatch('库存明细更新 SQL 不得更新子作业 ID', updateDetail, /job_detail_id\s*=/)
|
||||
assertNotMatch('库存明细更新 SQL 不得更新入库子单 ID', updateDetail, /stockin_detail_id\s*=/)
|
||||
|
||||
const createStockoutView = read('ruoyi-ui/src/views/out/create/index.vue')
|
||||
const createStockQuery = section(createStockoutView, 'stockQuery: {', 'selectedStocks:')
|
||||
assertMatch('出库创建页候选库存必须固定筛选正常库存状态', createStockQuery, /stockStatus:\s*'1'/)
|
||||
assertMatch('出库创建页候选库存必须固定筛选正常物料状态', createStockQuery, /productStatus:\s*'1'/)
|
||||
assertMatch('出库创建页必须使用双正常筛选对象查询库存', createStockoutView, /listStockDetail\(this\.stockQuery\)/)
|
||||
|
||||
const stockoutView = read('ruoyi-ui/src/views/out/stockout/index.vue')
|
||||
const manualStockQuery = section(stockoutView, 'listStockDetail({', '}).then(res =>')
|
||||
assertMatch('手动分配候选库存必须筛选正常库存状态', manualStockQuery, /stockStatus:\s*'1'/)
|
||||
assertMatch('手动分配候选库存必须筛选正常物料状态', manualStockQuery, /productStatus:\s*'1'/)
|
||||
|
||||
console.log('WCS、库存与出库需求静态检查通过')
|
||||
140
scripts/verify_dense_cargo_space_plan.py
Normal file
140
scripts/verify_dense_cargo_space_plan.py
Normal file
@@ -0,0 +1,140 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
|
||||
FRONTEND_URL = "http://localhost"
|
||||
BACKEND_URL = os.environ.get("DENSE_PLAN_BACKEND_URL", "http://127.0.0.1:8080")
|
||||
TEST_USERNAME = os.environ.get("DENSE_PLAN_USERNAME")
|
||||
TEST_PASSWORD = os.environ.get("DENSE_PLAN_PASSWORD")
|
||||
SCREENSHOT_PATH = Path(
|
||||
os.environ.get(
|
||||
"DENSE_PLAN_SCREENSHOT",
|
||||
r"C:\Users\13623\.codex\visualizations\2026\07\14\019f5e36-a2f0-7751-86cc-3d8ac317f956\dense-cargo-space-plan.png",
|
||||
)
|
||||
)
|
||||
|
||||
if not TEST_USERNAME or not TEST_PASSWORD:
|
||||
raise RuntimeError("请通过 DENSE_PLAN_USERNAME 和 DENSE_PLAN_PASSWORD 提供测试账号")
|
||||
|
||||
|
||||
def choose_warehouse(page, warehouse_name):
|
||||
warehouse_field = page.locator(".el-form-item").filter(has_text="仓库").first
|
||||
warehouse_field.locator(".el-select").click()
|
||||
page.locator(".el-select-dropdown:visible .el-select-dropdown__item").filter(
|
||||
has_text=warehouse_name
|
||||
).click()
|
||||
|
||||
|
||||
def assert_no_internal_overflow(page):
|
||||
for selector in (".dense-map__viewport", ".dense-grid"):
|
||||
metrics = page.locator(selector).evaluate(
|
||||
"node => ({ scrollWidth: node.scrollWidth, clientWidth: node.clientWidth, "
|
||||
"scrollHeight: node.scrollHeight, clientHeight: node.clientHeight })"
|
||||
)
|
||||
assert metrics["scrollWidth"] <= metrics["clientWidth"] + 1, (selector, metrics)
|
||||
assert metrics["scrollHeight"] <= metrics["clientHeight"] + 1, (selector, metrics)
|
||||
|
||||
|
||||
with sync_playwright() as playwright:
|
||||
browser = playwright.chromium.launch(
|
||||
headless=True,
|
||||
executable_path=r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
||||
)
|
||||
context = browser.new_context(viewport={"width": 1500, "height": 1050})
|
||||
page = context.new_page()
|
||||
console_errors = []
|
||||
page.on("console", lambda message: console_errors.append(message.text) if message.type == "error" else None)
|
||||
|
||||
login_response = page.request.post(
|
||||
f"{BACKEND_URL}/login",
|
||||
data={"username": TEST_USERNAME, "password": TEST_PASSWORD, "code": "", "uuid": ""},
|
||||
)
|
||||
login_data = login_response.json()
|
||||
assert login_data.get("code") == 200, login_data
|
||||
token = login_data["token"]
|
||||
context.add_cookies(
|
||||
[{"name": "Admin-Token", "value": token, "url": FRONTEND_URL}]
|
||||
)
|
||||
|
||||
def route_backend(route):
|
||||
request = route.request
|
||||
parsed = urlparse(request.url)
|
||||
backend_path = parsed.path.removeprefix("/dev-api")
|
||||
target_url = f"{BACKEND_URL}{backend_path}"
|
||||
if parsed.query:
|
||||
target_url += f"?{parsed.query}"
|
||||
response = page.request.fetch(
|
||||
target_url,
|
||||
method=request.method,
|
||||
headers=request.headers,
|
||||
data=request.post_data,
|
||||
)
|
||||
route.fulfill(response=response)
|
||||
|
||||
page.route("**/dev-api/**", route_backend)
|
||||
page.goto(f"{FRONTEND_URL}/kpi/planKpi")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
choose_warehouse(page, "密集库")
|
||||
dense_map = page.get_by_test_id("dense-cargo-space-map")
|
||||
dense_map.wait_for(state="visible")
|
||||
page.wait_for_function(
|
||||
"document.querySelectorAll('[data-testid=dense-location]').length === 259"
|
||||
)
|
||||
|
||||
floor_one_count = page.get_by_test_id("dense-location").count()
|
||||
mother_count = page.locator('[data-testid="dense-location"][data-status="9"]').count()
|
||||
lift_count = page.locator('[data-testid="dense-location"][data-status="7"]').count()
|
||||
conveyor_count = page.locator('[data-testid="dense-location"][data-status="8"]').count()
|
||||
charging_count = page.locator('[data-testid="dense-location"][data-status="10"]').count()
|
||||
assert (floor_one_count, mother_count, lift_count, conveyor_count, charging_count) == (259, 49, 2, 2, 1)
|
||||
assert page.get_by_text("充电桩", exact=True).count() >= 1
|
||||
assert page.locator(".axis-label--column").count() == 24
|
||||
assert page.locator(".axis-label--column", has_text="25").count() == 0
|
||||
assert page.locator(".search-box").get_by_test_id("floor-switch").count() == 1
|
||||
assert page.locator(".search-box").get_by_test_id("dense-refresh").count() == 1
|
||||
assert page.get_by_text("密集库模式:按层展示完整平面坐标", exact=True).count() == 0
|
||||
assert page.get_by_text("密集库货位平面图", exact=True).count() == 0
|
||||
assert_no_internal_overflow(page)
|
||||
assert page.locator('[data-testid="dense-location"][data-platoon="7"][data-column="13"]').count() == 1
|
||||
assert page.locator('[data-testid="dense-location"][data-platoon="1"][data-column="13"]').count() == 1
|
||||
assert page.locator('[data-testid="dense-location"][data-platoon="13"][data-column="13"]').count() == 1
|
||||
page.locator('[data-testid="dense-location"][data-platoon="1"][data-column="1"]').click()
|
||||
assert page.get_by_text("货区 A", exact=True).count() == 1
|
||||
assert page.get_by_text("区域 Z01", exact=True).count() == 0
|
||||
|
||||
page.get_by_test_id("floor-switch").get_by_text("2层", exact=True).click()
|
||||
page.wait_for_function(
|
||||
"Array.from(document.querySelectorAll('[data-testid=dense-location]')).every(node => node.textContent.trim().endsWith('02'))"
|
||||
)
|
||||
floor_two_count = page.get_by_test_id("dense-location").count()
|
||||
assert floor_two_count == 259
|
||||
assert page.locator('[data-testid="dense-location"][data-status="10"]').count() == 1
|
||||
|
||||
page.set_viewport_size({"width": 1280, "height": 800})
|
||||
page.wait_for_timeout(300)
|
||||
assert_no_internal_overflow(page)
|
||||
|
||||
SCREENSHOT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
dense_map.screenshot(path=str(SCREENSHOT_PATH))
|
||||
|
||||
choose_warehouse(page, "托盘库1")
|
||||
page.locator(".chart-container--normal").wait_for(state="visible")
|
||||
page.locator(".chart-container--normal canvas").first.wait_for(state="visible")
|
||||
assert page.get_by_test_id("dense-cargo-space-map").count() == 0
|
||||
|
||||
relevant_errors = [
|
||||
error for error in console_errors
|
||||
if "favicon" not in error.lower() and "sockjs" not in error.lower()
|
||||
]
|
||||
assert not relevant_errors, relevant_errors
|
||||
print(
|
||||
"dense-plan-ok "
|
||||
f"floor1={floor_one_count} floor2={floor_two_count} "
|
||||
f"mother={mother_count} lift={lift_count} conveyor={conveyor_count} charging={charging_count} "
|
||||
"legacy-chart=visible console-errors=0"
|
||||
)
|
||||
browser.close()
|
||||
Reference in New Issue
Block a user