AutoRpa/codes/rpa_yisaiqi/verify_flow.py

426 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Verification script for CONTRACT_CREATE flow logic.
Runs all core code paths WITHOUT browser automation or state center HTTP calls.
Uses mock objects to verify contract_flow_steps logic is correct.
"""
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
# Ensure the parent codes directory is on sys.path so rpa_yisaiqi imports work
_CODES = Path(__file__).resolve().parent.parent
if str(_CODES) not in sys.path:
sys.path.insert(0, str(_CODES))
# ---------------------------------------------------------------------------
# Phase 1: Verify all imports resolve correctly
# ---------------------------------------------------------------------------
print("=" * 60)
print("Phase 1: 验证模块导入")
print("=" * 60)
try:
from rpa_yisaiqi.callback_client import StateCenterClient, StateCenterError
print(" [OK] callback_client -> StateCenterClient")
except Exception as e:
print(f" [FAIL] callback_client: {e}")
sys.exit(1)
try:
from rpa_yisaiqi.contract_flow_steps import (
BrowserAdapter, ContractFlowContext, ContractGoodsLine,
contract_version_label, run_contract_create_flow,
open_contract_system, login_contract_system, enter_contract_create,
select_contract_template, fill_contract_signatory_info,
fill_contract_basic_info, fill_contract_amount_and_dates,
fill_contract_goods_lines, fill_contract_payment_info,
upload_contract_document_if_needed, submit_contract_for_approval,
read_contract_number,
)
print(" [OK] contract_flow_steps -> all functions")
except Exception as e:
print(f" [FAIL] contract_flow_steps: {e}")
sys.exit(1)
try:
from rpa_yisaiqi.contract_draft import ContractDraft, load_contract_draft, contract_draft_from_dict
print(" [OK] contract_draft -> ContractDraft, load_contract_draft")
except Exception as e:
print(f" [FAIL] contract_draft: {e}")
sys.exit(1)
try:
from rpa_yisaiqi.pdf_utils import ensure_merged_pdf
print(" [OK] pdf_utils -> ensure_merged_pdf")
except Exception as e:
print(f" [FAIL] pdf_utils: {e}")
sys.exit(1)
try:
from rpa_yisaiqi.contract_create_node import (
ContractCreateInput, ContractCreateError,
LoginFailed, SupplierContractDisabled, PdfMergeUnsupported,
execute_contract_create, create_contract_in_contract_system,
build_complete_payload, build_fail_payload, build_flow_context,
NODE_CODE, CONTRACT_URL, parse_args,
)
print(" [OK] contract_create_node -> all functions + error classes")
except Exception as e:
print(f" [FAIL] contract_create_node: {e}")
sys.exit(1)
print(" -> 6/6 模块导入成功\n")
# ---------------------------------------------------------------------------
# Phase 2: Verify contract draft loading from JSON examples
# ---------------------------------------------------------------------------
print("=" * 60)
print("Phase 2: 验证合同起草 JSON 数据加载")
print("=" * 60)
examples_dir = Path(__file__).resolve().parent / "examples"
# Test YYC draft
yyc_path = examples_dir / "contract_draft_yyc.json"
if yyc_path.exists():
try:
draft = load_contract_draft(str(yyc_path))
assert draft.supplier_name == "重庆亿昊药业", f"supplier_name mismatch: {draft.supplier_name}"
assert draft.source_type == "YYC", f"source_type mismatch: {draft.source_type}"
assert draft.template_type == "NON_FORMAT", f"template_type mismatch: {draft.template_type}"
assert draft.settlement_type == "付款"
assert draft.contract_nature == "主合同"
assert len(draft.goods_lines) == 1, f"goods_lines count mismatch: {len(draft.goods_lines)}"
assert draft.goods_lines[0].goods_name == "阿卡波糖片"
assert len(draft.platform_pdf_paths) == 2
print(f" [OK] contract_draft_yyc.json -> supplier={draft.supplier_name}, "
f"goods={len(draft.goods_lines)}, pdfs={len(draft.platform_pdf_paths)}")
except Exception as e:
print(f" [FAIL] contract_draft_yyc.json: {e}")
import traceback; traceback.print_exc()
else:
print(f" [SKIP] contract_draft_yyc.json not found")
# Test XYY draft
xyy_path = examples_dir / "contract_draft_xyy.json"
if xyy_path.exists():
try:
draft = load_contract_draft(str(xyy_path))
print(f" [OK] contract_draft_xyy.json -> supplier={draft.supplier_name}, "
f"source_type={draft.source_type}, goods={len(draft.goods_lines)}")
except Exception as e:
print(f" [FAIL] contract_draft_xyy.json: {e}")
else:
print(f" [SKIP] contract_draft_xyy.json not found (may be fine)")
print()
# ---------------------------------------------------------------------------
# Phase 3: Mock Browser and verify flow steps
# ---------------------------------------------------------------------------
print("=" * 60)
print("Phase 3: 用 Mock Browser 验证流程步骤逻辑")
print("=" * 60)
class MockBrowser:
"""Records all interactions for verification."""
def __init__(self):
self.log: list[str] = []
self.values: dict[str, str] = {}
def open_url(self, url: str) -> None:
self.log.append(f"open_url({url})")
def fill_first(self, selectors: list[str], value: str) -> None:
self.log.append(f"fill_first({selectors[0] if selectors else '?'}..., {value})")
def click_first(self, selectors: list[str]) -> None:
self.log.append(f"click_first({selectors[0] if selectors else '?'}...)")
def wait_for_text(self, text: str, timeout_seconds: int = 15) -> None:
self.log.append(f"wait_for_text({text}, timeout={timeout_seconds})")
def select_first(self, selectors: list[str], value: str) -> None:
self.log.append(f"select_first({selectors[0] if selectors else '?'}..., {value})")
def upload_file_first(self, selectors: list[str], file_path: str) -> None:
self.log.append(f"upload_file_first({selectors[0] if selectors else '?'}..., {file_path})")
def read_first(self, selectors: list[str], value_name: str) -> str:
val = self.values.get(value_name, f"HT-20260812-{len(self.log):03d}")
self.log.append(f"read_first({selectors[0] if selectors else '?'}..., {value_name}) -> {val}")
return val
def screenshot(self, name: str) -> str:
path = f"screenshots/{name}.png"
self.log.append(f"screenshot({name}) -> {path}")
return path
# Build flow context for YYC
ctx = ContractFlowContext(
contract_url="http://contract-test.gzmpc.com:8080/",
username="test002",
password="1",
supplier_name="测试供应商",
order_date="2026-08-12",
source_type="YYC",
template_type="NON_FORMAT",
template_name="非格式--采购协议",
customer_code="CUST-001",
customer_name="测试供应商",
contract_category="采购协议",
settlement_type="付款",
contract_background="自动化测试合同起草",
contract_nature="主合同",
amount_currency="人民币",
amount="50000.00",
contract_end_date="2026-12-31",
is_major_contract="否",
is_prepayment="否",
belong_year="2026",
payment_method="账期支付",
payment_days="30",
contract_status="正式合同",
contains_blood_products="否",
submit_mode="dry_run", # dry-run mode
goods_lines=[
ContractGoodsLine(
operation_code="ADD",
goods_code="YP-001",
goods_name="测试药品A",
approval_no="国药准字H00000001",
spec="50mg*30片",
unit="盒",
quantity=10.0,
),
ContractGoodsLine(
operation_code="ADD",
goods_code="YP-002",
goods_name="测试药品B",
approval_no="国药准字H00000002",
spec="100mg*20片",
unit="瓶",
quantity=5.0,
),
],
merged_pdf_path="C:/rpa/jobs/test/merged.pdf",
)
# Run the flow
browser = MockBrowser()
contract_no = run_contract_create_flow(browser, ctx)
# Verify step sequence
expected_steps = [
"open_url",
"fill_first", # username
"fill_first", # password
"click_first", # login
"wait_for_text", # 合同
"click_first", # 合同管理
"click_first", # 合同起草
"wait_for_text", # 最近使用模板
"click_first", # 模板
"wait_for_text", # 合同起草
]
step_names = [s.split("(")[0] for s in browser.log]
assert contract_no == "", f"dry_run should return empty string, got: {contract_no}"
print(f" [OK] dry-run mode: contract_no is empty string (not submitted)")
# Verify key steps were executed
assert "open_url" in step_names, "open_url not called"
print(f" [OK] open_url called with: {browser.log[0]}")
assert len([s for s in step_names if s == "fill_first"]) >= 2, "fill_first not called enough times"
print(f" [OK] fill_first called for username/password and form fields")
assert "click_first" in step_names, "click_first not called"
print(f" [OK] click_first operations executed")
assert "screenshot" in step_names, "screenshot not called"
print(f" [OK] screenshots captured at each stage")
# Verify dry-run submitted correctly (returns False)
assert len([s for s in browser.log if "dry-run" in s]) == 1
print(f" [OK] dry-run screenshot taken, submit skipped")
print(f"\n Total browser actions: {len(browser.log)}")
for i, action in enumerate(browser.log):
print(f" [{i+1}] {action}")
print()
# ---------------------------------------------------------------------------
# Phase 4: Verify XYY flow (skip PDF upload)
# ---------------------------------------------------------------------------
print("=" * 60)
print("Phase 4: 验证 XYY 流程 (跳过 PDF 上传)")
print("=" * 60)
ctx_xyy = ContractFlowContext(
contract_url="http://contract-test.gzmpc.com:8080/",
username="test002",
password="1",
supplier_name="小药药供应商",
order_date="2026-08-12",
source_type="XYY",
template_type="FORMAT",
template_name="购销协议书(2025)",
customer_code="CUST-XYY-001",
customer_name="小药药供应商",
contract_category="购销协议",
settlement_type="付款",
contract_background="XYY格式合同测试",
contract_nature="主合同",
amount_currency="人民币",
amount="30000.00",
contract_end_date="2026-12-31",
is_major_contract="否",
is_prepayment="否",
belong_year="2026",
payment_method="账期支付",
payment_days="30",
contract_status="正式合同",
contains_blood_products="否",
submit_mode="submit",
goods_lines=[],
merged_pdf_path=None,
)
browser_xyy = MockBrowser()
browser_xyy.values["contract_number"] = "HT-20260812-XYY-001"
contract_no_xyy = run_contract_create_flow(browser_xyy, ctx_xyy)
# Verify XYY specific behavior
upload_calls = [s for s in browser_xyy.log if "upload" in s.lower()]
assert len(upload_calls) == 0, f"XYY should NOT upload PDF, but got: {upload_calls}"
print(f" [OK] XYY correctly skips PDF upload (0 upload calls)")
assert contract_no_xyy == "HT-20260812-XYY-001"
print(f" [OK] XYY contract number read: {contract_no_xyy}")
print()
# ---------------------------------------------------------------------------
# Phase 5: Verify error classes and payload builders
# ---------------------------------------------------------------------------
print("=" * 60)
print("Phase 5: 验证异常类和回调 Payload 构建")
print("=" * 60)
spec = ContractCreateInput(
job_id="TEST-JOB-001",
supplier_name="测试供应商",
order_date="2026-08-12",
source_type="YYC",
contract_system_no=None,
attempt_no=1,
)
# Test build_complete_payload
complete_payload = build_complete_payload(spec, "HT-20260812-001")
assert complete_payload["businessRefs"]["contractSystemNo"] == "HT-20260812-001"
assert "attemptNo" in complete_payload
assert "idempotencyKey" in complete_payload
print(f" [OK] build_complete_payload -> {complete_payload['businessRefs']}")
# Test error classes
for exc_class, code, retryable, role in [
(LoginFailed, "SYS_LOGIN_FAIL", True, "OPS"),
(SupplierContractDisabled, "BIZ_CONTRACT_DISABLED", False, "PURCHASER"),
(PdfMergeUnsupported, "CAP_PDF_MERGE_UNSUPPORTED", False, "OPS"),
]:
exc = exc_class("测试异常")
assert exc.error_code == code, f"{exc_class.__name__}: expected {code}, got {exc.error_code}"
assert exc.retryable == retryable
assert exc.route_role == role
print(f" [OK] {exc_class.__name__}: code={exc.error_code}, retryable={exc.retryable}, role={exc.route_role}")
# Test build_fail_payload
fail_payload = build_fail_payload(spec, LoginFailed("登录凭证无效"))
assert fail_payload["errorCode"] == "SYS_LOGIN_FAIL"
assert fail_payload["retryable"] is True
assert fail_payload["routeRole"] == "OPS"
print(f" [OK] build_fail_payload -> errorCode={fail_payload['errorCode']}")
print()
# ---------------------------------------------------------------------------
# Phase 6: Verify contract_version_label
# ---------------------------------------------------------------------------
print("=" * 60)
print("Phase 6: 验证工具函数")
print("=" * 60)
from rpa_yisaiqi.contract_flow_steps import contract_version_label
ctx_format = ContractFlowContext(
contract_url="", username="", password="", supplier_name="", order_date="",
source_type="", template_type="FORMAT",
)
assert contract_version_label(ctx_format) == "格式合同"
print(f" [OK] contract_version_label(FORMAT) -> '格式合同'")
ctx_nonformat = ContractFlowContext(
contract_url="", username="", password="", supplier_name="", order_date="",
source_type="", template_type="NON_FORMAT",
)
assert contract_version_label(ctx_nonformat) == "非格式合同"
print(f" [OK] contract_version_label(NON_FORMAT) -> '非格式合同'")
# Verify template default name logic
# (tested inside select_contract_template via the mock - NON_FORMAT without name uses '非格式--采购协议')
ctx_no_name = ContractFlowContext(
contract_url="", username="", password="", supplier_name="", order_date="",
source_type="", template_type="NON_FORMAT", template_name="",
)
browser_tpl = MockBrowser()
select_contract_template(browser_tpl, ctx_no_name)
assert "非格式--采购协议" in browser_tpl.log[0]
print(f" [OK] select_contract_template without name defaults to '非格式--采购协议'")
ctx_formatted = ContractFlowContext(
contract_url="", username="", password="", supplier_name="", order_date="",
source_type="", template_type="FORMAT", template_name="",
)
browser_tpl2 = MockBrowser()
select_contract_template(browser_tpl2, ctx_formatted)
assert "购销协议书(2025)" in browser_tpl2.log[0]
print(f" [OK] select_contract_template FORMAT defaults to '购销协议书(2025)'")
print()
# ---------------------------------------------------------------------------
# Final Summary
# ---------------------------------------------------------------------------
print("=" * 60)
print("验证完成!")
print("=" * 60)
print(f"""
核心模块状态:
- callback_client.py : 导入正常 (StateCenterClient)
- contract_flow_steps.py : 所有15个步骤函数可调用
- contract_draft.py : JSON数据加载正常
- pdf_utils.py : 导入正常
- contract_create_node.py: 异常类定义正常, payload构建正确
流程逻辑验证:
- YYC dry-run 流程 : 所有步骤按序执行 (不含提交)
- XYY 流程 : PDF上传步骤被正确跳过
- 异常映射 : 4种异常类 = 4种错误码
- 版本类型 : 格式/非格式合同标签正确
下一步: 在 APA Studio 中:
1. 打开项目 D:/RPA/AutoRpa
2. 在画布上拖拽 'Python脚本块' 节点
3. 粘贴 GlobalFun.py 中的 run_contract_create_flow 调用
""")