工作台概览小卡 + 侧栏入口;总览/对方汇总/明细/原始流水抽屉接入 HEL-175/176 接口;无期初时展示期间净变动;模块内用 info 品蓝。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
287 lines
10 KiB
Python
287 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""HEL-177: 转账往来页 360/820/1440 冒烟截图(mock 接口,不依赖登录库)。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import threading
|
|
from functools import partial
|
|
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
from pathlib import Path
|
|
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
WEB = ROOT / "web"
|
|
OUT = ROOT / "hel177-shots"
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
|
|
VIEWPORTS = [(1440, 900, "1440"), (820, 900, "820"), (360, 800, "360")]
|
|
|
|
SUMMARY = {
|
|
"status": "ok",
|
|
"own_company": {"id": 3, "name": "河南金牛煤业有限公司"},
|
|
"window": {
|
|
"start": "2026-01-01",
|
|
"end": "2026-08-20",
|
|
"has_opening": False,
|
|
"opening": None,
|
|
"ending": None,
|
|
},
|
|
"confirmed": {
|
|
"outflow_total": "123860000.00",
|
|
"outflow_count": 48,
|
|
"inflow_total": "184205000.00",
|
|
"inflow_count": 77,
|
|
"net_change": "-60345000.00",
|
|
"net_direction": "payable",
|
|
},
|
|
"pending": {"count": 3, "amount_total": "3200000.00"},
|
|
"counterparties": [
|
|
{
|
|
"company_id": 5,
|
|
"company_name": "河南金牛置业有限公司",
|
|
"confirmed_outflow": "52000000.00",
|
|
"confirmed_inflow": "86000000.00",
|
|
"net": "-34000000.00",
|
|
"pending_count": 2,
|
|
"last_effective_at": "2026-08-18",
|
|
},
|
|
{
|
|
"company_id": 6,
|
|
"company_name": "河南金牛贸易有限公司",
|
|
"confirmed_outflow": "10000000.00",
|
|
"confirmed_inflow": "12605000.00",
|
|
"net": "-2605000.00",
|
|
"pending_count": 1,
|
|
"last_effective_at": "2026-08-10",
|
|
},
|
|
{
|
|
"company_id": 7,
|
|
"company_name": "河南金牛农业科技发展有限公司",
|
|
"confirmed_outflow": "61860000.00",
|
|
"confirmed_inflow": "85600000.00",
|
|
"net": "-23740000.00",
|
|
"pending_count": 0,
|
|
"last_effective_at": "2026-07-30",
|
|
},
|
|
],
|
|
}
|
|
|
|
# 修正 mock:按 HEL-169 净变动=转出-转入,正数应收
|
|
SUMMARY["confirmed"] = {
|
|
"outflow_total": "184205000.00",
|
|
"outflow_count": 80,
|
|
"inflow_total": "123860000.00",
|
|
"inflow_count": 48,
|
|
"net_change": "60345000.00",
|
|
"net_direction": "receivable",
|
|
}
|
|
SUMMARY["counterparties"] = [
|
|
{
|
|
"company_id": 5,
|
|
"company_name": "河南金牛置业有限公司",
|
|
"confirmed_outflow": "86000000.00",
|
|
"confirmed_inflow": "52000000.00",
|
|
"net": "34000000.00",
|
|
"pending_count": 2,
|
|
"last_effective_at": "2026-08-18",
|
|
},
|
|
{
|
|
"company_id": 6,
|
|
"company_name": "河南金牛贸易有限公司",
|
|
"confirmed_outflow": "12605000.00",
|
|
"confirmed_inflow": "10000000.00",
|
|
"net": "2605000.00",
|
|
"pending_count": 1,
|
|
"last_effective_at": "2026-08-10",
|
|
},
|
|
{
|
|
"company_id": 7,
|
|
"company_name": "河南金牛农业科技发展有限公司",
|
|
"confirmed_outflow": "85600000.00",
|
|
"confirmed_inflow": "61860000.00",
|
|
"net": "23740000.00",
|
|
"pending_count": 0,
|
|
"last_effective_at": "2026-07-30",
|
|
},
|
|
]
|
|
|
|
EVENTS = {
|
|
"status": "ok",
|
|
"window": {"start": "2026-01-01", "end": "2026-08-20"},
|
|
"events": [
|
|
{
|
|
"event_id": 101,
|
|
"direction": "in",
|
|
"state": "confirmed",
|
|
"pairing": "paired",
|
|
"locked": False,
|
|
"amount": "12000000.00",
|
|
"currency": "CNY",
|
|
"effective_at": "2026-08-18",
|
|
"summary": "周转资金调拨",
|
|
"status": "matched",
|
|
},
|
|
{
|
|
"event_id": 102,
|
|
"direction": "out",
|
|
"state": "pending",
|
|
"pairing": "unilateral",
|
|
"locked": False,
|
|
"amount": "1500000.00",
|
|
"currency": "CNY",
|
|
"effective_at": "2026-08-12",
|
|
"summary": "工程款结算(待确认)",
|
|
"status": "unresolved",
|
|
},
|
|
],
|
|
"next_cursor": None,
|
|
"has_more": False,
|
|
}
|
|
|
|
DETAIL = {
|
|
"status": "ok",
|
|
"event": {
|
|
"event_id": 101,
|
|
"classification": "intercompany",
|
|
"pairing": "paired",
|
|
"status": "matched",
|
|
"amount": "12000000.00",
|
|
"currency": "CNY",
|
|
"effective_at": "2026-08-18",
|
|
"mode": "auto",
|
|
"reason": "双边匹配",
|
|
"counterparty": {
|
|
"company_id": 5,
|
|
"company_name": "河南金牛置业有限公司",
|
|
"account_number_masked": "****8821",
|
|
},
|
|
"observations": [
|
|
{
|
|
"source_row_id": 1,
|
|
"role": "payee",
|
|
"source_row": 148,
|
|
"sheet_name": "流水",
|
|
"transaction_at": "2026-08-18 10:22:00",
|
|
"income": "12000000.00",
|
|
"expense": None,
|
|
"import_batch_id": 19,
|
|
"original_filename": "工行3305_202608.xls",
|
|
"own_account_masked": "****3305",
|
|
"counterparty_account_masked": "****8821",
|
|
"counterparty_name": "河南金牛置业有限公司",
|
|
"summary": "周转资金调拨",
|
|
"reference": "JS20260818-00317",
|
|
}
|
|
],
|
|
},
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
handler = partial(SimpleHTTPRequestHandler, directory=str(WEB))
|
|
httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
|
port = httpd.server_address[1]
|
|
threading.Thread(target=httpd.serve_forever, daemon=True).start()
|
|
base = f"http://127.0.0.1:{port}"
|
|
|
|
mock_js = f"""
|
|
window.__HEL177_MOCK__ = true;
|
|
const SUMMARY = {json.dumps(SUMMARY, ensure_ascii=False)};
|
|
const EVENTS = {json.dumps(EVENTS, ensure_ascii=False)};
|
|
const DETAIL = {json.dumps(DETAIL, ensure_ascii=False)};
|
|
const _fetch = window.fetch.bind(window);
|
|
window.fetch = async (input, init) => {{
|
|
const url = String(input);
|
|
if (url.includes('/api/me') || url.includes('/api/session')) {{
|
|
return new Response(JSON.stringify({{role:'company', company_id:3, username:'牛女士', company_name:'河南金牛煤业有限公司'}}), {{status:200, headers:{{'Content-Type':'application/json'}}}});
|
|
}}
|
|
if (url.includes('/api/company/workspace')) {{
|
|
return new Response(JSON.stringify({{status:'ok', pending_unilateral:0, pending_total:0, unilateral_events:[]}}), {{status:200, headers:{{'Content-Type':'application/json'}}}});
|
|
}}
|
|
if (url.includes('/api/company/intercompany/summary')) {{
|
|
return new Response(JSON.stringify(SUMMARY), {{status:200, headers:{{'Content-Type':'application/json'}}}});
|
|
}}
|
|
if (url.includes('/api/company/intercompany/events')) {{
|
|
return new Response(JSON.stringify(EVENTS), {{status:200, headers:{{'Content-Type':'application/json'}}}});
|
|
}}
|
|
if (url.includes('/api/company/transfer-events/')) {{
|
|
return new Response(JSON.stringify(DETAIL), {{status:200, headers:{{'Content-Type':'application/json'}}}});
|
|
}}
|
|
if (url.includes('/api/company/')) {{
|
|
return new Response(JSON.stringify({{status:'ok'}}), {{status:200, headers:{{'Content-Type':'application/json'}}}});
|
|
}}
|
|
return _fetch(input, init);
|
|
}};
|
|
"""
|
|
|
|
console_errors: list[str] = []
|
|
with sync_playwright() as p:
|
|
browser = p.chromium.launch()
|
|
page = browser.new_page()
|
|
page.on("pageerror", lambda exc: console_errors.append(f"pageerror:{exc}"))
|
|
page.on("console", lambda msg: console_errors.append(f"console:{msg.type}:{msg.text}") if msg.type == "error" else None)
|
|
|
|
for width, height, label in VIEWPORTS:
|
|
page.set_viewport_size({"width": width, "height": height})
|
|
page.add_init_script(mock_js)
|
|
# bypass auth guard
|
|
page.add_init_script(
|
|
"""
|
|
const orig = window.fetch;
|
|
// initAuthGuard 读 /api/me;已在 mock 中处理
|
|
"""
|
|
)
|
|
page.goto(f"{base}/company.html", wait_until="domcontentloaded")
|
|
page.evaluate(
|
|
"""() => {
|
|
// 若鉴权把页面踢走,强制停留
|
|
if (!document.body || document.body.dataset.portal !== 'company') return;
|
|
}"""
|
|
)
|
|
# 等 summary 渲染
|
|
page.wait_for_timeout(600)
|
|
# 切到转账往来
|
|
page.evaluate("() => { if (typeof showView === 'function') showView('transfers'); }")
|
|
page.wait_for_selector("#transfersData:not([hidden])", timeout=5000)
|
|
page.wait_for_timeout(300)
|
|
overflow = page.evaluate(
|
|
"() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1"
|
|
)
|
|
assert not overflow, f"{label} overview overflow"
|
|
# 文案:期间净变动,无期末余额误用
|
|
net_title = page.inner_text("#tfStatNetTitle")
|
|
assert "期间净变动" in net_title, net_title
|
|
page.screenshot(path=str(OUT / f"overview-{label}.png"), full_page=True)
|
|
|
|
# 下钻明细
|
|
page.click("#transfersCpBody tr.clickable")
|
|
page.wait_for_selector("#transfersDetailLayer:not([hidden])", timeout=5000)
|
|
page.wait_for_timeout(300)
|
|
overflow2 = page.evaluate(
|
|
"() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1"
|
|
)
|
|
assert not overflow2, f"{label} detail overflow"
|
|
page.screenshot(path=str(OUT / f"detail-{label}.png"), full_page=True)
|
|
|
|
# 打开抽屉
|
|
page.click("[data-transfer-evidence]")
|
|
page.wait_for_selector("#transferEvidenceDrawer.is-open", timeout=5000)
|
|
page.wait_for_timeout(200)
|
|
page.screenshot(path=str(OUT / f"drawer-{label}.png"), full_page=True)
|
|
page.click("[data-close-transfer-evidence]")
|
|
|
|
browser.close()
|
|
|
|
hard = [e for e in console_errors if "Failed to load resource" not in e and "favicon" not in e]
|
|
print("shots:", list(OUT.glob("*.png")))
|
|
print("console_errors:", hard)
|
|
if hard:
|
|
raise SystemExit(1)
|
|
print("OK")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|