feat: 库存看板增加 BOM 物料齐套视角 + BOM 详情显示当前在库/状态
- 库存看板新增「BOM 物料齐套」标签页(仅 admin/manager), 逐棵 BOM 列出组成物料的 单套用量/当前在库/状态, 并给出整体齐套结论 - BOM 详情页组成物料表新增「当前在库」「状态」两列 - 抽出统一 _status_of() 判定函数, 红/黄/绿逻辑与看板一致 - 新增 tools/verify_bom_board.py 端到端验证脚本(已通过) - 新增使用教程 物料入库出库系统_使用教程.md - .gitignore 忽略 _backup/(旧 EXE 备份)
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
"""验证: 库存看板 BOM 齐套标签页 + BOM 详情页在库/状态列。
|
||||
|
||||
不依赖已存在的 app.db, 自建临时库 + 种子数据做断言。
|
||||
"""
|
||||
import os, sys, tempfile, importlib.util
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(HERE)
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
# 用临时库, 不污染正式数据
|
||||
tmp = tempfile.mktemp(suffix=".db")
|
||||
os.environ["WMS_DB"] = tmp
|
||||
|
||||
import app as appmod
|
||||
from models import db, Material, Bom, BomItem, Inbound
|
||||
from datetime import date
|
||||
|
||||
appmod.seed_admin()
|
||||
with appmod.app.app_context():
|
||||
m = Material(part_number="P0001", manufacturer="测试IC-A", unit="颗", safety_stock=100)
|
||||
db.session.add(m); db.session.flush()
|
||||
b = Bom(code="T03", name="测试产品T03"); db.session.add(b); db.session.flush()
|
||||
db.session.add(BomItem(bom_id=b.id, material_id=m.id, qty_per_unit=2))
|
||||
# 入库 30, 安全库存 100 -> 在库 30 < 100 -> 偏低(warn)
|
||||
db.session.add(Inbound(material_id=m.id, qty=30, source="测试", occur_date=date.today()))
|
||||
db.session.commit()
|
||||
BID = b.id # 在上下文内取出 id, 避免外部访问 detached 实例
|
||||
|
||||
client = appmod.app.test_client()
|
||||
# 登录 admin
|
||||
r = client.post("/login", data={"username": "admin", "password": "admin123"}, follow_redirects=True)
|
||||
assert r.status_code == 200, f"login failed: {r.status_code}"
|
||||
|
||||
# 1) 看板页应包含 BOM 齐套标签(用 id="tab-bom" 区块判断, 而非注释文本)
|
||||
r = client.get("/dashboard")
|
||||
html = r.get_data(as_text=True)
|
||||
assert "库存预警" in html, "看板缺少库存预警标签"
|
||||
assert 'id="tab-bom"' in html, "看板缺少 BOM 齐套标签页"
|
||||
print("[OK] 管理员看板页含 BOM 齐套标签页")
|
||||
|
||||
# 2) BOM 详情页应包含 当前在库 / 状态 列, 且 P0001 偏低
|
||||
r = client.get(f"/bom/{BID}")
|
||||
html = r.get_data(as_text=True)
|
||||
assert "当前在库" in html, "BOM 详情缺少 当前在库 列"
|
||||
assert "状态" in html, "BOM 详情缺少 状态 列"
|
||||
assert "P0001" in html, "BOM 详情未展示组成物料编号"
|
||||
assert "偏低" in html, "P0001 在库30<安全100 应显示偏低, 实际未标黄"
|
||||
assert "30.00" in html, "BOM 详情未显示正确在库数量"
|
||||
print("[OK] BOM 详情页展示 当前在库/状态, P0001 正确标为偏低")
|
||||
|
||||
# 3) operator 角色不应看到 BOM 标签 (权限隔离)
|
||||
from models import User
|
||||
import hashlib
|
||||
def sha256(s): return hashlib.sha256(s.encode()).hexdigest()
|
||||
with appmod.app.app_context():
|
||||
if not db.session.scalar(db.select(User).where(User.username == "op1")):
|
||||
db.session.add(User(username="op1", password=sha256("op123"), role="operator", display_name="操作工"))
|
||||
db.session.commit()
|
||||
r = client.post("/logout")
|
||||
r = client.post("/login", data={"username": "op1", "password": "op123"}, follow_redirects=True)
|
||||
r = client.get("/dashboard")
|
||||
html = r.get_data(as_text=True)
|
||||
assert 'id="tab-bom"' not in html, "operator 不应看到 BOM 齐套标签"
|
||||
print("[OK] operator 角色看不到 BOM 齐套标签(权限隔离正确)")
|
||||
|
||||
os.remove(tmp)
|
||||
print("\nALL PASS ✅")
|
||||
Reference in New Issue
Block a user