491 lines
18 KiB
Python
491 lines
18 KiB
Python
import pytest
|
|
|
|
from rpa_yisaiqi.contract_flow_steps import (
|
|
ContractFlowContext,
|
|
ContractGoodsLine,
|
|
fill_contract_signatory_info,
|
|
run_contract_create_flow,
|
|
)
|
|
from rpa_yisaiqi.runtime.errors import SupplierContractDisabled
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def no_sleep(monkeypatch):
|
|
monkeypatch.setattr("rpa_yisaiqi.contract_flow_steps.time.sleep", lambda _s: None)
|
|
|
|
|
|
class FakeBrowser:
|
|
def __init__(self):
|
|
self.calls = []
|
|
self.body_text = ""
|
|
self.xform_labels = {}
|
|
self.filled = {}
|
|
self.read_values = {
|
|
"contract_number": "HT-20260811-REAL-001",
|
|
}
|
|
|
|
def open_url(self, url):
|
|
self.calls.append(("open_url", url))
|
|
if "kmAgreementApply" in url or "draft.jsp" in url:
|
|
self.body_text = "客户编码 客户名称 合同类别"
|
|
|
|
def fill_first(self, selectors, value):
|
|
self.calls.append(("fill_first", tuple(selectors), value))
|
|
for selector in selectors:
|
|
self.filled[selector] = value
|
|
|
|
def click_native(self, selectors):
|
|
self.calls.append(("click_native", tuple(selectors)))
|
|
|
|
def click_first(self, selectors):
|
|
self.calls.append(("click_first", tuple(selectors)))
|
|
joined = " ".join(str(item) for item in selectors)
|
|
if "login" in joined or "登录" in joined:
|
|
self.body_text = "切换门户 待办 待阅 合同管理 合同起草"
|
|
|
|
def wait_for_text(self, text, timeout_seconds=15):
|
|
self.calls.append(("wait_for_text", text, timeout_seconds))
|
|
if text and text not in self.body_text:
|
|
self.body_text = f"{self.body_text} {text}".strip()
|
|
|
|
def wait_for_element(self, selectors, timeout_seconds=15):
|
|
self.calls.append(("wait_for_element", tuple(selectors), timeout_seconds))
|
|
|
|
def element_exists(self, selectors):
|
|
self.calls.append(("element_exists", tuple(selectors)))
|
|
return True
|
|
|
|
def switch_to_frame(self, selectors):
|
|
self.calls.append(("switch_to_frame", tuple(selectors)))
|
|
|
|
def switch_to_default_content(self):
|
|
self.calls.append(("switch_to_default_content",))
|
|
|
|
def select_first(self, selectors, value):
|
|
self.calls.append(("select_first", tuple(selectors), value))
|
|
for selector in selectors:
|
|
self.filled[selector] = value
|
|
|
|
def select_xform(self, name, value):
|
|
self.calls.append(("select_xform", name, value))
|
|
self.xform_labels[name] = value
|
|
|
|
def execute_script(self, script, *args):
|
|
self.calls.append(("execute_script", script[:48], args))
|
|
if "hasTextInFrames" in script:
|
|
return True
|
|
if "latestByApplyTime" in script and "headerIdx" in script:
|
|
return "001132026081734940"
|
|
if "wantedTitle" in script and "headerIdx" in script:
|
|
return "001132026081734940"
|
|
if "fd_opcode" in script and "return i" in script:
|
|
return 0
|
|
if "List_Selected" in script and "innerText" in script:
|
|
token = str(args[0]) if args else ""
|
|
if token.startswith("DW"):
|
|
self.filled['input[name="extendDataFormInfo.value(fd_customeropcode)"]'] = token
|
|
elif token:
|
|
self.filled['input[name="extendDataFormInfo.value(fd_detail1.0.fd_opcode)"]'] = token
|
|
if "box.click" in script:
|
|
return {"ok": True}
|
|
return True
|
|
if "marked" in script and "fd_detail1" in script:
|
|
return []
|
|
if "return location.href" in script:
|
|
return "http://contract-test.gzmpc.com:8080/km/agreement/km_agreement_apply/kmAgreementApply.do?method=view"
|
|
if "Com_Parameter" in script and "fdId" in script:
|
|
return "FD-TEST-001"
|
|
if "添加合同" in script and ("pdf" in script or "wantedStem" in script):
|
|
if any(call[0] == "upload_file_first" for call in self.calls):
|
|
return {"ok": True, "via": "name"}
|
|
return {"ok": False, "via": "empty"}
|
|
if "提交" in script or "合同正文" in script or "确定" in script:
|
|
return {"ok": True}
|
|
if "fs-label" in script and args:
|
|
return self.xform_labels.get(args[0], "")
|
|
if "data-value" in script:
|
|
return "模板A"
|
|
return []
|
|
|
|
def upload_file_first(self, selectors, file_path):
|
|
self.calls.append(("upload_file_first", tuple(selectors), file_path))
|
|
|
|
def read_first(self, selectors, value_name):
|
|
self.calls.append(("read_first", tuple(selectors), value_name))
|
|
if value_name in self.read_values:
|
|
return self.read_values[value_name]
|
|
for selector in selectors:
|
|
if selector in self.filled:
|
|
return self.filled[selector]
|
|
joined = " ".join(str(item) for item in selectors)
|
|
if "fd_customername" in joined:
|
|
return self.read_values.get("customer_name", "重庆亿昊药业有限公司")
|
|
if "fd_customeropcode" in joined:
|
|
return self.read_values.get("customer_code", self.filled.get(
|
|
'input[name="extendDataFormInfo.value(fd_customeropcode)"]', ""
|
|
))
|
|
if "fdSettype" in joined:
|
|
return self.read_values.get("settlement_type", "付款")
|
|
return ""
|
|
|
|
def read_body_text(self):
|
|
self.calls.append(("read_body_text",))
|
|
return self.body_text
|
|
|
|
def screenshot(self, name):
|
|
self.calls.append(("screenshot", name))
|
|
return f"/tmp/{name}.png"
|
|
|
|
|
|
def test_signatory_rejects_placeholder_customer_before_opening_picker():
|
|
browser = FakeBrowser()
|
|
ctx = ContractFlowContext(
|
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
|
username="test002",
|
|
password="1",
|
|
supplier_name="重庆亿昊药业有限公司",
|
|
order_date="2026-08-11",
|
|
source_type="YYC",
|
|
customer_code="CUST-YYC-001",
|
|
customer_name="重庆亿昊药业有限公司",
|
|
)
|
|
|
|
with pytest.raises(SupplierContractDisabled, match="占位符"):
|
|
fill_contract_signatory_info(browser, ctx)
|
|
|
|
assert browser.calls == []
|
|
|
|
|
|
def test_contract_flow_logs_in_with_real_contract_login_selectors():
|
|
browser = FakeBrowser()
|
|
ctx = ContractFlowContext(
|
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
|
username="test002",
|
|
password="1",
|
|
supplier_name="重庆亿昊药业有限公司",
|
|
order_date="2026-08-11",
|
|
source_type="XYY",
|
|
template_type="FORMAT",
|
|
customer_code="DW0100046361",
|
|
customer_name="重庆亿昊药业有限公司",
|
|
goods_lines=[],
|
|
)
|
|
|
|
run_contract_create_flow(browser, ctx)
|
|
|
|
assert ("open_url", "http://contract-test.gzmpc.com:8080/") in browser.calls
|
|
assert ("fill_first", ('input[name="j_username"]', 'input[placeholder="用户名"]'), "test002") in browser.calls
|
|
assert ("fill_first", ('input[name="j_password"]', 'input[placeholder="密码"]'), "1") in browser.calls
|
|
assert (
|
|
"click_first",
|
|
('a[class*="login_button"]', 'a:contains("登录")', 'input[name="btn_submit"]'),
|
|
) in browser.calls
|
|
assert not [call for call in browser.calls if call[0] == "screenshot"]
|
|
|
|
|
|
def _touch_pdf(path):
|
|
path.write_bytes(b"%PDF-1.4\n1 0 obj<<>>endobj\ntrailer<<>>\n%%EOF\n")
|
|
return str(path)
|
|
|
|
|
|
def test_contract_flow_uses_order_date_and_uploads_yyc_merged_pdf(tmp_path):
|
|
browser = FakeBrowser()
|
|
browser.read_values["customer_name"] = "重庆亿昊药业有限公司"
|
|
pdf_path = _touch_pdf(tmp_path / "contract-merged.pdf")
|
|
ctx = ContractFlowContext(
|
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
|
username="test002",
|
|
password="1",
|
|
supplier_name="重庆亿昊药业有限公司",
|
|
order_date="2026-08-11",
|
|
source_type="YYC",
|
|
template_type="FORMAT",
|
|
customer_code="DW0100046361",
|
|
customer_name="重庆亿昊药业有限公司",
|
|
goods_lines=[
|
|
ContractGoodsLine(
|
|
operation_code="ADD",
|
|
goods_code="FQX0100118047",
|
|
goods_name="盐酸托烷司琼注射液",
|
|
approval_no="苏20160372",
|
|
spec="5ml:5mg*5支",
|
|
unit="盒",
|
|
quantity=12,
|
|
unit_price="50.00",
|
|
)
|
|
],
|
|
merged_pdf_path=pdf_path,
|
|
)
|
|
|
|
contract_no = run_contract_create_flow(browser, ctx)
|
|
|
|
assert contract_no == "001132026081734940"
|
|
assert ("fill_first", ('input[name="fdSigndate"]',), "2026-08-11") in browser.calls
|
|
assert ("fill_first", ('input[name="fdEffedate"]',), "2026-08-11") in browser.calls
|
|
assert (
|
|
"click_first",
|
|
('div[title="选择商品"]', "div.lui-component.lui_widget_btn_txt", "text=选择商品"),
|
|
) in browser.calls
|
|
assert (
|
|
"switch_to_frame",
|
|
(
|
|
'iframe[src*="EopBasedataMaterial"]',
|
|
'iframe[src*="dialog_fdMaterial"]',
|
|
'iframe[src*="common-template"]',
|
|
),
|
|
) in browser.calls
|
|
assert ("fill_first", ("input.search-box-text", "input[placeholder*='关键字']"), "FQX0100118047") in browser.calls
|
|
assert (
|
|
"fill_first",
|
|
('input[name="extendDataFormInfo.value(fd_detail1.0.fd_goodsqty)"]',),
|
|
"12",
|
|
) in browser.calls
|
|
assert (
|
|
"fill_first",
|
|
('input[name="extendDataFormInfo.value(fd_detail1.0.fd_unitprice)"]',),
|
|
"50.00",
|
|
) in browser.calls
|
|
assert any(
|
|
call[0] == "upload_file_first" and call[-1] == pdf_path
|
|
for call in browser.calls
|
|
)
|
|
|
|
|
|
def test_contract_flow_skips_pdf_upload_for_xyy():
|
|
browser = FakeBrowser()
|
|
browser.read_values["customer_name"] = "重庆亿昊药业有限公司"
|
|
ctx = ContractFlowContext(
|
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
|
username="test002",
|
|
password="1",
|
|
supplier_name="重庆亿昊药业有限公司",
|
|
order_date="2026-08-11",
|
|
source_type="XYY",
|
|
template_type="FORMAT",
|
|
customer_code="DW0100046361",
|
|
customer_name="重庆亿昊药业有限公司",
|
|
goods_lines=[],
|
|
merged_pdf_path="C:/should-not-upload.pdf",
|
|
)
|
|
|
|
run_contract_create_flow(browser, ctx)
|
|
|
|
assert not [call for call in browser.calls if call[0] == "upload_file_first"]
|
|
|
|
|
|
def test_contract_flow_enters_draft_form_by_template_url(tmp_path):
|
|
from rpa_yisaiqi.contract_flow_steps import FORM_PAGE_URL
|
|
|
|
browser = FakeBrowser()
|
|
browser.read_values["customer_name"] = "重庆亿昊药业有限公司"
|
|
ctx = ContractFlowContext(
|
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
|
username="test002",
|
|
password="1",
|
|
supplier_name="重庆亿昊药业有限公司",
|
|
order_date="2026-08-11",
|
|
source_type="YYC",
|
|
template_type="NON_FORMAT",
|
|
template_name="非格式--采购协议",
|
|
customer_code="DW0100046361",
|
|
customer_name="重庆亿昊药业有限公司",
|
|
goods_lines=[],
|
|
merged_pdf_path=_touch_pdf(tmp_path / "contract-merged.pdf"),
|
|
)
|
|
|
|
run_contract_create_flow(browser, ctx)
|
|
|
|
assert ("open_url", FORM_PAGE_URL) in browser.calls
|
|
assert ("wait_for_text", "客户编码", 20) in browser.calls
|
|
|
|
|
|
def test_contract_flow_fills_real_contract_draft_form_fields_from_screenshot():
|
|
browser = FakeBrowser()
|
|
browser.read_values["customer_name"] = "重庆亿昊药业有限公司"
|
|
ctx = ContractFlowContext(
|
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
|
username="test002",
|
|
password="1",
|
|
supplier_name="重庆亿昊药业有限公司",
|
|
order_date="2026-08-11",
|
|
source_type="XYY",
|
|
template_type="NON_FORMAT",
|
|
template_name="非格式--采购协议",
|
|
customer_code="DW0100046361",
|
|
customer_name="重庆亿昊药业有限公司",
|
|
contract_category="采购协议",
|
|
settlement_type="付款",
|
|
contract_background="按订单自动起草采购协议",
|
|
contract_nature="主合同",
|
|
amount_currency="人民币",
|
|
amount="12000.50",
|
|
contract_end_date="2026-12-31",
|
|
is_major_contract="否",
|
|
is_prepayment="否",
|
|
belong_year="2026",
|
|
payment_method="账期支付",
|
|
payment_days="30",
|
|
contract_status="正式合同",
|
|
contains_blood_products="否",
|
|
goods_lines=[],
|
|
)
|
|
|
|
run_contract_create_flow(browser, ctx)
|
|
|
|
assert (
|
|
"select_xform",
|
|
"extendDataFormInfo.value(fd_contract_cate_type)",
|
|
"其他协议(采购)",
|
|
) in browser.calls
|
|
assert ("select_first", ('select#fdSettype',), "付款") in browser.calls
|
|
assert (
|
|
"fill_first",
|
|
('textarea[name="extendDataFormInfo.value(fd_desc_info)"]',),
|
|
"按订单自动起草采购协议",
|
|
) in browser.calls
|
|
assert ("select_xform", "extendDataFormInfo.value(fd_all_amount_type)", "合同标的金额") in browser.calls
|
|
assert ("fill_first", ('input[name="fdAccount"]',), "12000.50") in browser.calls
|
|
assert ("fill_first", ('input[name="fdSigndate"]',), "2026-08-11") in browser.calls
|
|
assert ("fill_first", ('input[name="fdEffedate"]',), "2026-08-11") in browser.calls
|
|
assert ("fill_first", ('input[name="fdEnddate"]',), "2026-12-31") in browser.calls
|
|
assert ("select_xform", "extendDataFormInfo.value(fd_is_major_contract)", "否") in browser.calls
|
|
assert ("select_xform", "extendDataFormInfo.value(fd_advance_amout)", "否") in browser.calls
|
|
assert (
|
|
"fill_first",
|
|
('input[name="extendDataFormInfo.value(fd_contract_year_text)"]',),
|
|
"2026",
|
|
) in browser.calls
|
|
assert ("select_xform", "extendDataFormInfo.value(fd_paytype)", "电汇") in browser.calls
|
|
assert (
|
|
"fill_first",
|
|
('input[name="extendDataFormInfo.value(fd_pay_time_limit)"]',),
|
|
"30",
|
|
) in browser.calls
|
|
assert ("select_xform", "extendDataFormInfo.value(fd_suconstatus)", "正式合同") in browser.calls
|
|
assert ("select_xform", "extendDataFormInfo.value(fd_3d1ce3953f1162)", "否") in browser.calls
|
|
|
|
|
|
def test_contract_flow_dry_run_skips_submit_but_still_reads_page_state():
|
|
browser = FakeBrowser()
|
|
browser.read_values["customer_name"] = "重庆亿昊药业有限公司"
|
|
ctx = ContractFlowContext(
|
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
|
username="test002",
|
|
password="1",
|
|
supplier_name="重庆亿昊药业有限公司",
|
|
order_date="2026-08-11",
|
|
source_type="XYY",
|
|
template_type="NON_FORMAT",
|
|
template_name="非格式--采购协议",
|
|
customer_code="DW0100046361",
|
|
customer_name="重庆亿昊药业有限公司",
|
|
submit_mode="dry_run",
|
|
goods_lines=[],
|
|
)
|
|
|
|
contract_no = run_contract_create_flow(browser, ctx)
|
|
|
|
assert not [call for call in browser.calls if call[0] == "click_first" and "text=提交" in call[1]]
|
|
assert not [call for call in browser.calls if call[0] == "screenshot"]
|
|
assert contract_no == ""
|
|
|
|
|
|
def test_contract_flow_writes_text_accept_report_instead_of_requiring_screenshots(tmp_path, monkeypatch):
|
|
from rpa_yisaiqi.contract_flow_steps import _ACCEPT, accept_log_path
|
|
|
|
monkeypatch.setenv("RPA_ACCEPT_LOG", str(tmp_path / "ACCEPT.txt"))
|
|
browser = FakeBrowser()
|
|
browser.read_values["customer_name"] = "重庆亿昊药业有限公司"
|
|
ctx = ContractFlowContext(
|
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
|
username="test002",
|
|
password="1",
|
|
supplier_name="重庆亿昊药业有限公司",
|
|
order_date="2026-08-11",
|
|
source_type="XYY",
|
|
template_type="NON_FORMAT",
|
|
customer_code="DW0100046361",
|
|
customer_name="重庆亿昊药业有限公司",
|
|
submit_mode="dry_run",
|
|
goods_lines=[],
|
|
)
|
|
|
|
run_contract_create_flow(browser, ctx)
|
|
|
|
text = accept_log_path("CONTRACT_CREATE").read_text(encoding="utf-8")
|
|
assert "ACCEPT SUMMARY" in text
|
|
assert "ACCEPT S03 ok" in text
|
|
assert "ACCEPT S06 ok" in text
|
|
assert "customerName=重庆亿昊药业有限公司" in text
|
|
assert "ACCEPT S12 ok" in text
|
|
assert all(line.ok for line in _ACCEPT.lines)
|
|
|
|
|
|
def test_read_contract_number_falls_back_to_my_contracts_list():
|
|
from rpa_yisaiqi.contract_flow_steps import read_contract_number
|
|
|
|
browser = FakeBrowser()
|
|
browser.read_values.pop("contract_number", None)
|
|
ctx = ContractFlowContext(
|
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
|
username="test002",
|
|
password="1",
|
|
supplier_name="重庆亿昊药业有限公司",
|
|
order_date="2026-08-11",
|
|
source_type="XYY",
|
|
customer_code="DW0100046361",
|
|
customer_name="重庆亿昊药业有限公司",
|
|
contract_title="test1",
|
|
goods_lines=[],
|
|
)
|
|
|
|
contract_no = read_contract_number(browser, ctx)
|
|
|
|
assert contract_no == "001132026081734940"
|
|
assert ("open_url", "http://contract-test.gzmpc.com:8080/") in browser.calls
|
|
assert ("click_first", ("text=我的合同",)) in browser.calls
|
|
assert ("click_first", ("text=我起草的",)) in browser.calls
|
|
assert ("click_first", ("text=申请时间",)) in browser.calls
|
|
|
|
|
|
def test_read_contract_number_goes_to_my_list_even_without_ctx_arg():
|
|
from rpa_yisaiqi import _flow_state as state
|
|
from rpa_yisaiqi.contract_flow_steps import read_contract_number
|
|
|
|
browser = FakeBrowser()
|
|
browser.read_values.pop("contract_number", None)
|
|
old_ctx = state.ctx
|
|
state.ctx = ContractFlowContext(
|
|
contract_url="http://contract-test.gzmpc.com:8080/",
|
|
username="test002",
|
|
password="1",
|
|
supplier_name="重庆亿昊药业有限公司",
|
|
order_date="2026-08-11",
|
|
source_type="XYY",
|
|
customer_code="DW0100046361",
|
|
customer_name="重庆亿昊药业有限公司",
|
|
contract_title="test1",
|
|
goods_lines=[],
|
|
)
|
|
try:
|
|
contract_no = read_contract_number(browser)
|
|
finally:
|
|
state.ctx = old_ctx
|
|
|
|
assert contract_no == "001132026081734940"
|
|
assert ("click_first", ("text=我的合同",)) in browser.calls
|
|
assert ("click_first", ("text=申请时间",)) in browser.calls
|
|
|
|
|
|
def test_accept_check_fails_when_page_value_mismatches():
|
|
from rpa_yisaiqi.contract_flow_steps import AcceptanceReport
|
|
from rpa_yisaiqi.runtime.errors import NodeError
|
|
|
|
report = AcceptanceReport()
|
|
with pytest.raises(NodeError, match="ACCEPT S06 FAIL"):
|
|
report.check(
|
|
"S06",
|
|
"签约方",
|
|
[("customerName", "其他公司", "重庆亿昊药业有限公司")],
|
|
)
|