fix(report): 报表统计修复批次——对账口径、导出审计与月份校验
- 部门汇总全公司 5% 按每位有台账聚合行的营销经理各计一次, 团队应发小计取聚合额 + 调整净额;与月结 total_payable 对拍 (有调整单时 + adjust_net_total),分列舍入差单列 - 人员归属按行类型拆分(普通/经理个人户/仅调整单),调整单全量并入, 团队与未分配不重复;按经理筛选只汇总展示团队 - 导出改单 sheet 分块签字稿,审计覆盖取数与超限;筛选页眉 ID→名称 - 月份校验抽 core/month,报表/月结/调整单非法月份统一 400; 岗位只读选项供财务/领导筛选 - 前端修复:部门下钻带入筛选、回款多桶独立状态、打印截断判定、 summary 请求序号保护 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
294d99a0d3
commit
ebb3a92f25
21 files changed
+2377
-287
No files matched your search
+143
-7
@@ -23,6 +23,7 @@ import os
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from decimal import Decimal
|
||||
|
||||
from playwright.sync_api import Page, sync_playwright
|
||||
|
||||
@@ -74,6 +75,15 @@ def step_setup_via_api(page: Page) -> dict:
|
||||
if not token:
|
||||
fail("API 预置", "localStorage 中未取到 ys_token")
|
||||
|
||||
# 回款统计断言取增量:脚本可重复运行(上次失败/历史 e2e 数据会累计到桶)
|
||||
baseline = _api(
|
||||
"GET", f"/api/v1/reports/payment-stats?settlement_month={MONTH}", token
|
||||
)
|
||||
baseline_bucket = next(b for b in baseline["buckets"] if b["code"] == "61-90")
|
||||
expected_receipt_amount = (
|
||||
Decimal(str(baseline_bucket["receipt_amount"])) + Decimal("10000.00")
|
||||
)
|
||||
|
||||
roles = _api("GET", "/api/v1/system/business-roles?pageNum=1&pageSize=50", token)["list"]
|
||||
salesperson_role = next((r["id"] for r in roles if r["role_code"] == "salesperson"), None)
|
||||
manager_role = next((r["id"] for r in roles if r["role_code"] == "sales_manager"), None)
|
||||
@@ -159,8 +169,10 @@ def step_setup_via_api(page: Page) -> dict:
|
||||
return {
|
||||
"month": MONTH,
|
||||
"business_no": business["business_no"],
|
||||
"personnel_id": person["id"],
|
||||
"personnel_name": person["personnel_name"],
|
||||
"manager_name": manager["personnel_name"],
|
||||
"expected_receipt_amount": f"{expected_receipt_amount:.2f}",
|
||||
}
|
||||
|
||||
|
||||
@@ -216,7 +228,7 @@ def step_business_stats(page: Page, ctx: dict) -> None:
|
||||
row = page.locator(".el-table__row", has_text=ctx["business_no"]).first
|
||||
if row.count() == 0:
|
||||
fail("业务统计", f"未找到业务 {ctx['business_no']}")
|
||||
print("✓ 6. 业务统计 关键字筛选命中")
|
||||
print("✓ 7. 业务统计 关键字筛选命中")
|
||||
|
||||
|
||||
def step_payment_stats(page: Page, ctx: dict) -> None:
|
||||
@@ -225,16 +237,136 @@ def step_payment_stats(page: Page, ctx: dict) -> None:
|
||||
page.locator(".filters button:has-text('查询')").first.click()
|
||||
page.wait_for_timeout(900)
|
||||
|
||||
rows = page.locator(".el-table__row")
|
||||
# 页面同时有区间表与两个维度对比表,必须限定主区间表
|
||||
rows = page.locator('[data-testid="payment-bucket-table"] .el-table__row')
|
||||
if rows.count() != 8:
|
||||
fail("回款统计", f"区间表应为 8 行,实际 {rows.count()}")
|
||||
bucket = page.locator(".el-table__row", has_text="61-90 天").first
|
||||
bucket = page.locator(
|
||||
'[data-testid="payment-bucket-table"] .el-table__row', has_text="61-90 天"
|
||||
).first
|
||||
if bucket.count() == 0:
|
||||
fail("回款统计", "未找到 61-90 天区间行")
|
||||
bucket_text = bucket.inner_text()
|
||||
if "10000.00" not in bucket_text:
|
||||
fail("回款统计", f"61-90 天桶回款金额不符:{bucket_text[:120]}")
|
||||
print("✓ 7. 回款统计 8 档区间与目标桶金额正确")
|
||||
expected = ctx["expected_receipt_amount"]
|
||||
if expected not in bucket_text:
|
||||
fail("回款统计", f"61-90 天桶回款金额不符(期望含 {expected}):{bucket_text[:120]}")
|
||||
print("✓ 8. 回款统计 8 档区间与目标桶金额正确")
|
||||
|
||||
|
||||
def step_payment_multi_expand(page: Page, ctx: dict) -> None:
|
||||
"""多个区间同时展开时,各自的下钻面板不能串行显示同一批明细。"""
|
||||
page.goto(f"{BASE_URL}/reports/payment-stats", wait_until="networkidle")
|
||||
page.wait_for_timeout(900)
|
||||
page.locator(".filters button:has-text('查询')").first.click()
|
||||
page.wait_for_timeout(900)
|
||||
|
||||
for label in ("61-90 天", "1-30 天"):
|
||||
row = page.locator(
|
||||
'[data-testid="payment-bucket-table"] .el-table__row', has_text=label
|
||||
).first
|
||||
row.locator(".el-table__expand-icon").click()
|
||||
page.wait_for_timeout(700)
|
||||
|
||||
drills = page.locator(".drill-wrap")
|
||||
if drills.count() != 2:
|
||||
fail("回款多桶展开", f"应展开 2 个下钻面板,实际 {drills.count()}")
|
||||
texts = [drills.nth(index).inner_text() for index in range(2)]
|
||||
# 面板是明细行(每笔 10000),不是桶合计;串行时两个面板文本会完全相同
|
||||
if texts[0] == texts[1] or "10000.00" not in "".join(texts):
|
||||
fail("回款多桶展开", "两个桶面板显示了同一批明细(drill 状态串行)")
|
||||
print("✓ 9. 回款多桶展开 两个下钻面板互不串行")
|
||||
|
||||
|
||||
def step_department_drill(page: Page, ctx: dict) -> None:
|
||||
"""部门汇总「查看明细」必须把人员与月份带到个人明细页。"""
|
||||
page.goto(f"{BASE_URL}/reports/department-summary", wait_until="networkidle")
|
||||
page.wait_for_timeout(900)
|
||||
page.locator(".filters button:has-text('查询')").first.click()
|
||||
page.wait_for_timeout(900)
|
||||
|
||||
row = page.locator(".el-table__row", has_text=ctx["manager_name"]).first
|
||||
row.locator(".el-table__expand-icon").click()
|
||||
page.wait_for_timeout(500)
|
||||
page.locator("button:has-text('查看明细')").first.click()
|
||||
page.wait_for_timeout(900)
|
||||
|
||||
if f"personnel_id={ctx['personnel_id']}" not in page.url:
|
||||
fail("部门下钻", f"URL 未携带人员筛选:{page.url}")
|
||||
if ctx["personnel_name"] not in page.locator(".filters").inner_text():
|
||||
fail("部门下钻", "个人明细页未应用人员筛选")
|
||||
print("✓ 6. 部门下钻 人员与月份筛选已生效")
|
||||
|
||||
|
||||
def step_print_truncated(page: Page, ctx: dict) -> None:
|
||||
"""超过打印上限时必须给出截断提示(修复前提示永不出现)。"""
|
||||
|
||||
def handler(route) -> None:
|
||||
rows = [
|
||||
{
|
||||
"ledger_id": index,
|
||||
"business_no": f"E2E-{index}",
|
||||
"ship_date": None,
|
||||
"receipt_date": f"{ctx['month']}-01",
|
||||
"payment_days": 10,
|
||||
"product_category": "municipal_water",
|
||||
"customer_type": "personal",
|
||||
"role_name": "业务员",
|
||||
"rate_text": "1",
|
||||
"weight_r": 1,
|
||||
"performance_amount": "1.00",
|
||||
"payable_amount": "1.00",
|
||||
}
|
||||
for index in range(200)
|
||||
]
|
||||
route.fulfill(
|
||||
json={
|
||||
"code": 200,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"list": rows,
|
||||
"total": 2500,
|
||||
"pageNum": 1,
|
||||
"pageSize": 200,
|
||||
"summary": {"row_count": 2500},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
page.route("**/api/v1/reports/personal-detail*", handler)
|
||||
try:
|
||||
page.goto(
|
||||
f"{BASE_URL}/reports/print/personal-detail?settlement_month={ctx['month']}",
|
||||
wait_until="networkidle",
|
||||
)
|
||||
page.wait_for_timeout(1200)
|
||||
if page.locator(".truncated").count() == 0:
|
||||
fail("打印截断", "超过 2000 行未显示截断提示")
|
||||
if "2000" not in page.locator(".truncated").inner_text():
|
||||
fail("打印截断", "截断提示未说明上限行数")
|
||||
finally:
|
||||
page.unroute("**/api/v1/reports/personal-detail*", handler)
|
||||
print("✓ 11. 打印截断 超限提示可见")
|
||||
|
||||
|
||||
def step_print_department(page: Page, ctx: dict) -> None:
|
||||
"""部门汇总打印稿必须含团队分成、5% 块、对账、成员与未分配五块。"""
|
||||
page.goto(
|
||||
f"{BASE_URL}/reports/print/department-summary?settlement_month={ctx['month']}",
|
||||
wait_until="networkidle",
|
||||
)
|
||||
page.wait_for_timeout(1200)
|
||||
for testid in (
|
||||
"print-department-teams",
|
||||
"print-company-block",
|
||||
"print-reconciliation",
|
||||
"print-department-members",
|
||||
"print-unassigned",
|
||||
):
|
||||
if page.locator(f'[data-testid="{testid}"]').count() == 0:
|
||||
fail("部门打印", f"缺少区块 {testid}")
|
||||
if "财务复核" not in page.locator(".print-page").inner_text():
|
||||
fail("部门打印", "签字栏缺失")
|
||||
print("✓ 12. 部门打印 五块结构与签字栏完整")
|
||||
|
||||
|
||||
def step_print_view(page: Page, ctx: dict) -> None:
|
||||
@@ -253,7 +385,7 @@ def step_print_view(page: Page, ctx: dict) -> None:
|
||||
page.wait_for_timeout(400)
|
||||
if page.evaluate("window.__printed") is not True:
|
||||
fail("打印视图", "window.print 未被调用")
|
||||
print("✓ 8. 打印视图 表格/签字栏/打印调用均正常")
|
||||
print("✓ 10. 打印视图 表格/签字栏/打印调用均正常")
|
||||
|
||||
|
||||
def run(page: Page) -> None:
|
||||
@@ -266,9 +398,13 @@ def run(page: Page) -> None:
|
||||
ctx = step_setup_via_api(page)
|
||||
step_personal_detail(page, ctx)
|
||||
step_department_summary(page, ctx)
|
||||
step_department_drill(page, ctx)
|
||||
step_business_stats(page, ctx)
|
||||
step_payment_stats(page, ctx)
|
||||
step_payment_multi_expand(page, ctx)
|
||||
step_print_view(page, ctx)
|
||||
step_print_truncated(page, ctx)
|
||||
step_print_department(page, ctx)
|
||||
|
||||
if console_errors:
|
||||
print(f" (控制台错误 {len(console_errors)} 条:{console_errors[:2]})")
|
||||
|
||||
Reference in new issue
Block a user