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, PLANNER_DEFAULT, RebateFlowContext, _pick_person, build_header_draft, fill_rebate_header, header_draft_from_table3, import_lines_from_table3, run_header_flow, ) class FakeBrowser: def __init__(self, *, table_text="", body_text="", login_body="", 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.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 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)) def click_native(self, selectors: list[str]) -> None: self.click_first(selectors) 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: self.clicked.append([str(args[0] if args else "")]) 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_REBATE_ORDER_NO" in text: return self.order_no 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 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)) 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", ) 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" 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 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 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] 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: """计划员三个点打开自己的选择层,不拿货源选择当层。""" 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 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