- Flask + SQLAlchemy 物料/入库/出库/BOM/生产订单全模块管理 - PyInstaller 打包为单文件 EXE (双击运行, 自动建库) - 物料维度: 入库总数量 / 消耗总数量 / 逐次入库流水(含时间) - BOM 详情展示组成物料的规格型号与单套用量 - 支持 Altium / 嘉立创 BOM 批量导入(文本与 Excel 双通道) - 内置端到端验证脚本(tools/_exe_util.py 等, 清理进程树防残留)
159 lines
6.0 KiB
Python
159 lines
6.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""EXE 端到端验证(聚焦真实 BOM 模板支持):
|
|
1. 启动 MaterialWMS.exe
|
|
2. 登录
|
|
3. /boms 页面含 "批量导入窗口" + "Part Number" 真实格式说明
|
|
4. /template/bom 下载模板, 表头含 Part Number / Quantity
|
|
5. 生成多 sheet 真实格式 xlsx, POST /api/import/boms/preview (multipart) -> valid==2
|
|
6. POST /api/import/boms/commit -> created_materials >= 2
|
|
7. 校验 dist/app.db 中 Bom XM-A 存在
|
|
"""
|
|
import http.client, os, time, subprocess, sys, json
|
|
from openpyxl import Workbook
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from _exe_util import kill_dist_exes, start_exe, stop_exe
|
|
|
|
HOST, PORT = "127.0.0.1", 8080
|
|
EXE = r"D:\2026-09-10-task-20\material_wms\dist\MaterialWMS.exe"
|
|
DB = r"D:\2026-09-10-task-20\material_wms\dist\app.db"
|
|
|
|
|
|
def wait_port():
|
|
for _ in range(60):
|
|
try:
|
|
c = http.client.HTTPConnection(HOST, PORT, timeout=2)
|
|
c.request("GET", "/")
|
|
r = c.getresponse(); r.read(); c.close()
|
|
if r.status in (200, 302):
|
|
return
|
|
except Exception:
|
|
time.sleep(1)
|
|
raise RuntimeError("EXE 端口未起")
|
|
|
|
|
|
def req(method, path, body=None, headers=None):
|
|
c = http.client.HTTPConnection(HOST, PORT, timeout=15)
|
|
c.request(method, path, body=body, headers=headers or {})
|
|
r = c.getresponse()
|
|
raw = r.read()
|
|
setc = r.getheader("Set-Cookie")
|
|
c.close()
|
|
return r.status, raw, setc
|
|
|
|
|
|
def post_file(path, file_path, cookie):
|
|
with open(file_path, "rb") as f:
|
|
data = f.read()
|
|
boundary = "WMSBND"
|
|
body = (
|
|
f"--{boundary}\r\n"
|
|
f'Content-Disposition: form-data; name="file"; filename="{os.path.basename(file_path)}"\r\n'
|
|
f"Content-Type: application/octet-stream\r\n\r\n"
|
|
).encode() + data + f"\r\n--{boundary}--\r\n".encode()
|
|
hdrs = {"Content-Type": f"multipart/form-data; boundary={boundary}",
|
|
"Content-Length": str(len(body))}
|
|
if cookie:
|
|
hdrs["Cookie"] = cookie
|
|
return req("POST", path, body=body, headers=hdrs)
|
|
|
|
|
|
def login(cookie):
|
|
body = urllib_parse("username=admin&password=admin123")
|
|
return req("POST", "/login", body=body,
|
|
headers={"Content-Type": "application/x-www-form-urlencoded",
|
|
"Cookie": cookie or ""})
|
|
|
|
|
|
def urllib_parse(s):
|
|
import urllib.parse as up
|
|
return up.urlencode(dict(up.parse_qsl(s))).encode()
|
|
|
|
|
|
def main():
|
|
# 1. 启动
|
|
print("[1] 启动 EXE ...")
|
|
p = start_exe(EXE)
|
|
try:
|
|
wait_port()
|
|
print(" [OK] 端口就绪")
|
|
|
|
# 2. 登录
|
|
_, _, setc = req("GET", "/login")
|
|
cookie = setc.split(";")[0] if setc else ""
|
|
_, _, setc2 = login(cookie)
|
|
if setc2:
|
|
cookie = setc2.split(";")[0]
|
|
# 验证登录
|
|
s, html, _ = req("GET", "/dashboard", headers={"Cookie": cookie})
|
|
assert s == 200 and "库存看板" in html.decode("utf-8", "ignore"), "登录失败"
|
|
print(" [OK] 登录")
|
|
|
|
# 3. BOM 页面含按钮 + 真实格式说明
|
|
s, html, _ = req("GET", "/boms", headers={"Cookie": cookie})
|
|
html = html.decode("utf-8", "ignore")
|
|
if "批量导入窗口" not in html:
|
|
print("[DEBUG] /boms status:", s)
|
|
print("[DEBUG] HTML head:", html[:800])
|
|
raise AssertionError("BOM 页缺按钮")
|
|
assert "Part Number" in html, "BOM 页未说明真实格式"
|
|
assert "嘉立创" in html or "Altium" in html, "BOM 页未提及 Altium/嘉立创"
|
|
print(" [OK] BOM 页含按钮 + 真实格式说明")
|
|
|
|
# 4. 下载 BOM 模板
|
|
s, raw, _ = req("GET", "/template/bom", headers={"Cookie": cookie})
|
|
assert s == 200, f"模板下载 status={s}"
|
|
tmpdir = os.path.join(os.path.dirname(DB), "_check_tmp")
|
|
os.makedirs(tmpdir, exist_ok=True)
|
|
tpath = os.path.join(tmpdir, "_tpl_bom_check.xlsx")
|
|
with open(tpath, "wb") as f: f.write(raw)
|
|
from openpyxl import load_workbook
|
|
tws = load_workbook(tpath).active
|
|
hdr = [c.value for c in tws[1]]
|
|
assert "Part Number" in hdr and "Quantity" in hdr, hdr
|
|
print(f" [OK] BOM 模板表头: {hdr}")
|
|
|
|
# 5. 生成多 sheet 真实格式 xlsx
|
|
wb = Workbook()
|
|
ws1 = wb.active; ws1.title = "XM-A"
|
|
ws1.append(["Item", "Part Number", "Manufacture Part Number", "Quantity", "Reference", "Part", "Package"])
|
|
ws1.append([1, "EXA100", "MFA", 2, "U1", "MFA", "PKGA"])
|
|
ws1.append([2, "EXA101", "MFB", 1, "U2", "MFB", "PKGB"])
|
|
ws2 = wb.create_sheet("组装")
|
|
ws2.append(["序号", "层级", "物料代码", "描述", "数量", "材质"])
|
|
ws2.append([1, "0.1", "EXA200", "外壳", 1, "ABS"])
|
|
up = os.path.join(tmpdir, "_bom_check.xlsx")
|
|
wb.save(up)
|
|
|
|
# 6. 上传预览
|
|
s, raw, _ = post_file("/api/import/boms/preview", up, cookie)
|
|
pv = json.loads(raw.decode("utf-8", "ignore"))
|
|
codes = [b["code"] for b in pv["boms"]]
|
|
assert "XM-A" in codes and "组装" in codes, pv
|
|
assert pv["valid"] == 2, pv
|
|
print(f" [OK] 上传预览: 解析 {len(codes)} 个 BOM, valid={pv['valid']}")
|
|
|
|
# 7. 提交
|
|
body = json.dumps({"boms": pv["boms"]}).encode()
|
|
s, raw, _ = req("POST", "/api/import/boms/commit", body=body,
|
|
headers={"Content-Type": "application/json", "Cookie": cookie})
|
|
rs = json.loads(raw.decode("utf-8", "ignore"))
|
|
assert rs["boms"] == 2 and rs["created_materials"] >= 2, rs
|
|
print(f" [OK] 提交: boms={rs['boms']} added={rs['added_items']} created_materials={rs['created_materials']}")
|
|
|
|
# 8. 校验数据库
|
|
import sqlite3
|
|
con = sqlite3.connect(DB)
|
|
n = con.execute("select count(*) from bom where code in ('XM-A','组装')").fetchone()[0]
|
|
con.close()
|
|
assert n >= 2, f"DB 中 BOM 不足: {n}"
|
|
print(f" [OK] 数据库确认: BOM 表新增 {n} 条")
|
|
|
|
print("\nEXE 真实 BOM 模板 端到端验证通过")
|
|
finally:
|
|
stop_exe(p)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|