AutoRpa/rpa_rebate/tests/test_header.py

368 lines
14 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.

# 返利总单:表 3 MDM、计划员廖建琦、复用已有总单号。
from accept import accept_log_path
from apa_browser_adapter import chrome_debug_port, chrome_session_flow, resolve_chrome_profile_dir
from rebate_flow_steps import (
CONTACT_DEFAULT,
LOGIN,
PAY_MODE_NEXT_MONTH_N,
PAY_PARAM_DEFAULT,
PLANNER_DEFAULT,
RebateFlowContext,
_pick_person,
build_header_draft,
fill_rebate_header,
header_draft_from_table3,
import_lines_from_table3,
run_header_flow,
)
# 总单流程假浏览器,按脚本关键字回勾选和协议总单ID。
class FakeBrowser:
# 记下页面脚本回传的勾选、保存和协议号。
def __init__(
self,
*,
table_text="",
body_text="",
login_body="",
order_no="",
existing_order_no="",
):
self.table_text = table_text
self.body_text = body_text
self.login_body = login_body or body_text
self.order_no = order_no
self.existing_order_no = existing_order_no
self.saved = False
self.opened: list[str] = []
self.filled: list[tuple[list[str], str]] = []
self.selected: list[tuple[list[str], str]] = []
self.clicked: list[list[str]] = []
self.script_count = 0
# 记下打开过的 URL。
def open_url(self, url: str) -> None:
self.opened.append(url)
# 记下填过的选择器和值。
def fill_first(self, selectors: list[str], value: str) -> None:
self.filled.append((list(selectors), value))
# 记下点过的选择器。
def click_first(self, selectors: list[str]) -> None:
self.clicked.append(list(selectors))
# 记下原生点击,行为同 click_first。
def click_native(self, selectors: list[str]) -> None:
self.click_first(selectors)
# 假正文里没有这段字就抛 TimeoutError。
def wait_for_text(self, text: str, timeout_seconds: int = 15) -> None:
haystack = f"{self.body_text}\n{self.table_text}\n{self.login_body}"
if text not in haystack:
raise TimeoutError(text)
# 假页面一律当作元素已在。
def wait_for_element(self, selectors: list[str], timeout_seconds: int = 15) -> None:
return None
# 假页面一律当作元素已在。
def element_exists(self, selectors: list[str]) -> bool:
return True
# 返回构造时塞进的正文。
def read_body_text(self) -> str:
return self.body_text
# 按脚本标记回勾选、保存、协议号。
def execute_script(self, script: str, *args):
self.script_count += 1
text = script or ""
if "OPEN_HEADER_ROW" in text:
return {"ok": True, "via": "row"}
if "SELECT_HEADER_ROW" in text:
return {"ok": True, "text": str(args[0] if args else "")}
if "HEADER_SELECTION" in text:
no = str(args[0] if args else "")
return {"ok": True, "cells": [no], "contract": no, "checkedIdx": 0, "pager": "共1条"}
if "CLOSE_HEADER_QUERY" in text or "CLOSE_DETAIL_CARD" in text:
return {"ok": True}
if "CLICK_HEADER_TOOLBAR" in text:
title = str(args[0] if args else "")
self.clicked.append([title])
if title == "保存":
self.saved = True
return {"ok": True}
if "DETAIL_TOAST" in text:
return {"texts": []}
if "HEADER_CARD" in text:
return False
if "OPEN_LABELED_PICKER" in text:
return {"ok": True}
if "FILL_HEADER_LOOKUP" in text or "PICK_HEADER_LOOKUP" in text:
return {"ok": True}
if "CONFIRM_HEADER_CARD" in text:
return {"ok": True, "via": "确认"}
if "SET_HEADER_CELL" in text:
return {"ok": False}
if "SET_LABELED" in text:
label = args[0] if args else ""
value = args[1] if len(args) > 1 else ""
self.filled.append(([str(label)], str(value)))
return True
if "READ_LABELED_INPUT" in text:
title = str(args[0] if args else "")
if title == "承付参数":
return {"value": PAY_PARAM_DEFAULT}
if title == "计划员":
return {"value": "李四"}
return {"value": ""}
if "READ_REBATE_ORDER_NO" in text:
return self.existing_order_no or (self.order_no if self.saved else "")
return self.table_text
# 假截图,返回空路径。
def screenshot(self, name: str) -> str:
return name
# 假读取,返回空字符串。
def read_first(self, selectors: list[str], value_name: str = "") -> str:
return self.order_no
# 假切入 iframe,什么都不做。
def switch_to_frame(self, selectors: list[str]) -> None:
return None
# 假切回主文档,什么都不做。
def switch_to_default_content(self) -> None:
return None
# 记下选过的选择器和值。
def select_first(self, selectors: list[str], value: str) -> None:
self.selected.append((list(selectors), value))
# 记下 xform 选择。
def select_xform(self, name: str, value: str) -> None:
return None
# 记下上传路径。
def upload_file_first(self, selectors: list[str], file_path: str) -> None:
return None
# 构造一段假返利会话入参。
def _ctx() -> RebateFlowContext:
return RebateFlowContext(rebate_url="http://example", username="u", password="p")
# 构造一份带测试客商编码的总单草稿。
def _draft():
return build_header_draft(
planner_name="李四",
supplier_code="DW0100046361",
rebate_unit_code="901",
today="2026-10-09",
)
# 总单供应商用表 3 MDM,计划员仍是廖建琦,细单货品ID和备注来自表 3。
def test_header_draft_from_table3_uses_mdm_and_planner():
draft = header_draft_from_table3(
[
{
"supplier_id": "SUP001",
"supplier_mdm_code": "DW0100046361",
"planner_name": "李四",
"purchaser_name": "张三",
"org_code": "901",
"goods_id": "4000018689",
"qty": "10",
"unit_price": "10",
"oms_id": "20260928000003",
}
],
today="2026-10-12",
)
assert draft.supplier_code == "DW0100046361"
assert draft.planner_name == PLANNER_DEFAULT
assert draft.rebate_unit_code == "901"
assert draft.contact_name == CONTACT_DEFAULT
lines = import_lines_from_table3(
[
{
"goods_id": "4000018689",
"qty": "10",
"unit_price": "10",
"oms_id": "20260928000003",
"goods_source_id": "SRC1",
}
]
)
assert lines[0].goods_id == "4000018689"
assert lines[0].order_no == "20260928000003"
assert lines[0].tax_rate == "0.13"
assert lines[0].net_price == "8.8496"
# 总单默认走现行字段:下月N号结、协议起止当天、返利合约否。
def test_header_defaults_follow_mapping_not_scheme_55():
draft = _draft()
assert draft.planner_name == "李四"
assert draft.pay_mode == PAY_MODE_NEXT_MONTH_N
assert draft.protocol_start == "2026-10-09"
assert draft.protocol_end == "2026-10-09"
assert draft.rebate_contract == "否"
assert draft.settle_mode == "否"
# 计划员名为空时回退廖建琦,联系人苏文辉。
def test_header_draft_defaults_planner():
draft = build_header_draft(planner_name="", supplier_code="S1", rebate_unit_code="901")
assert draft.planner_name == PLANNER_DEFAULT
assert draft.contact_name == CONTACT_DEFAULT
# 填总单时用草稿里的计划员、结算和承付模式。
def test_fill_header_uses_mapping_values():
browser = FakeBrowser(
table_text="HT1 重庆亿昊",
body_text="返利合约 计划员 承付模式 保存",
)
fill_rebate_header(browser, _draft())
filled_values = [value for _, value in browser.filled]
selected_values = [value for _, value in browser.selected]
assert "李四" in filled_values
assert "2026-10-09" in filled_values
assert PAY_MODE_NEXT_MONTH_N in selected_values
assert "否" in selected_values
assert "廖建峰" not in filled_values
assert "账期" not in selected_values
assert "2026-12-31" not in filled_values
# 总单流程写 ACCEPT,带上协议总单ID。
def test_header_flow_writes_accept_with_order_no(tmp_path, monkeypatch):
monkeypatch.delenv("RPA_ACCEPT_LOG", raising=False)
monkeypatch.setattr("accept.SCREENSHOT_ROOT", tmp_path)
browser = FakeBrowser(
table_text="HT1 重庆亿昊",
body_text="采购协议管理 合同编号 查询 返利合约 承付模式 计划员 保存 总单号 FL20261009001",
login_body="采购协议管理",
order_no="FL20261009001",
)
order_no = run_header_flow(browser, _ctx(), "HT1", _draft())
assert order_no == "FL20261009001"
text = accept_log_path("REBATE_HEADER").read_text(encoding="utf-8")
assert "ACCEPT SUMMARY REBATE_HEADER 8/8 ok" in text
assert "ACCEPT S09 ok 读总单号" in text
assert "rebateOrderNo=FL20261009001" in text
assert "planner=李四" in text
assert f"payMode={PAY_MODE_NEXT_MONTH_N}" in text
assert "protocolEnd=2026-10-09" in text
# 表上已有协议总单ID则复用,不再新建。
def test_header_reuses_existing_order_no_without_create(tmp_path, monkeypatch):
monkeypatch.delenv("RPA_ACCEPT_LOG", raising=False)
monkeypatch.setattr("accept.SCREENSHOT_ROOT", tmp_path)
browser = FakeBrowser(
table_text="HT1 重庆亿昊 FL_EXISTING",
body_text="采购协议管理 合同编号 查询 返利合约 承付模式 计划员 保存 总单号 FL_EXISTING",
login_body="采购协议管理",
existing_order_no="FL_EXISTING",
)
order_no = run_header_flow(browser, _ctx(), "HT1", _draft())
assert order_no == "FL_EXISTING"
assert ["新增"] not in browser.clicked
assert ["保存"] in browser.clicked
text = accept_log_path("REBATE_HEADER").read_text(encoding="utf-8")
assert "rebateOrderNo=FL_EXISTING" in text
assert "reused=yes" in text
assert "ACCEPT S07 ok 填总单" in text
# 已登录会话不再填账密。
def test_header_reuses_logged_in_session(tmp_path, monkeypatch):
monkeypatch.delenv("RPA_ACCEPT_LOG", raising=False)
monkeypatch.setattr("accept.SCREENSHOT_ROOT", tmp_path)
browser = FakeBrowser(
table_text="HT1 重庆亿昊",
body_text="采购协议管理 待审批 合同编号 查询 返利合约 承付模式 计划员 保存",
login_body="采购协议管理",
order_no="FL1",
)
run_header_flow(browser, _ctx(), "HT1", _draft())
login_fills = [value for selectors, value in browser.filled if selectors == LOGIN["user"]]
assert login_fills == []
assert "u" not in [value for _, value in browser.filled]
# 总单和查号共用同一 Chrome 会话和端口。
def test_header_chrome_session_reuses_find():
assert chrome_session_flow("REBATE_HEADER") == "REBATE_FIND"
assert resolve_chrome_profile_dir("REBATE_HEADER") == resolve_chrome_profile_dir("REBATE_FIND")
assert chrome_debug_port("REBATE_HEADER") == chrome_debug_port("REBATE_FIND")
# 计划员三个点打开自己的选择层,不拿货源选择当层。
class PlannerPickBrowser:
# 记下计划员选择层是否打开、选中的姓名和 ID。
def __init__(self):
self.planner_name = ""
self.planner_id = ""
self.layer_open = False
self.scripts: list[str] = []
# 按脚本标记模拟计划员三个点弹层。
def execute_script(self, script: str, *args):
text = script or ""
self.scripts.append(text)
if "READ_LABELED_INPUT" in text:
title = str(args[0] if args else "")
if title == "计划员ID":
return {"value": self.planner_id}
if title == "计划员":
return {"value": self.planner_name}
return {"value": ""}
if "CLOSE_HEADER_QUERY" in text:
return {"ok": True}
if "OPEN_LABELED_PICKER" in text:
self.layer_open = True
return {"ok": True, "via": "more-outline"}
if "PERSON_PICKER_VISIBLE" in text:
return self.layer_open
if "FOCUS_PERSON_FIELD" in text:
return {"ok": True, "ph": "请输入计划员", "title": "计划员选择"}
if "CLICK_PERSON_QUERY" in text:
return {"ok": True, "via": "query"}
if "PERSON_PICKER_ROWS" in text:
return {"ok": True, "match": True, "count": 1, "row": "廖建琦"}
if "PICK_PERSON_ROW" in text:
self.planner_name = "廖建琦"
self.planner_id = "U1001"
self.layer_open = False
return {"ok": True, "text": "廖建琦"}
if "CLOSE_PERSON_PICKER" in text:
self.layer_open = False
return {"ok": True, "via": "close"}
if "SCAN_LOOKUP_OVERLAYS" in text:
return ["计划员选择"] if self.layer_open else []
if "SOURCE_CHOICE" in text or "CLOSE_SOURCE_CHOICE" in text:
return False
return None
# 计划员三个点按姓名选廖建琦并绑上 ID,不走货源细单。
def test_pick_person_selects_liao_and_binds_id():
browser = PlannerPickBrowser()
_pick_person(browser, "计划员", PLANNER_DEFAULT)
assert browser.planner_name == "廖建琦"
assert browser.planner_id == "U1001"
joined = "\n".join(browser.scripts)
assert "FOCUS_PERSON_FIELD" in joined
assert "PICK_PERSON_ROW" in joined
assert "FOCUS_SOURCE_FIELD" not in joined
assert "PICK_SOURCE_ROW" not in joined